diff --git a/.github/workflows/build-kits.yml b/.github/workflows/build-kits.yml new file mode 100644 index 000000000..7bcd88c5f --- /dev/null +++ b/.github/workflows/build-kits.yml @@ -0,0 +1,69 @@ +name: Build Kits + +on: + workflow_call: + workflow_dispatch: + +permissions: + contents: read + +jobs: + load-matrix: + name: Load kit matrix + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set.outputs.matrix }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + sparse-checkout: kits/matrix.json + sparse-checkout-cone-mode: false + - id: set + run: echo "matrix=$(jq -c . kits/matrix.json)" >> "$GITHUB_OUTPUT" + + build-kits: + name: Build ${{ matrix.kit.name }} + needs: load-matrix + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + kit: ${{ fromJson(needs.load-matrix.outputs.matrix) }} + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Set SDK version + run: | + echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV + + - name: Install JDK 17 + uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5 + with: + distribution: zulu + java-version: "17" + + - name: Publish core SDK to Maven Local + run: ./gradlew publishMavenPublicationToMavenLocal + + - name: Run kit unit tests + if: ${{ matrix.kit.skip_unit_tests != true }} + run: | + ./gradlew "${{ matrix.kit.kit_project }}:testRelease" \ + -c settings-kits.gradle \ + -Pmparticle.kit.mparticleFromMavenLocalOnly=true + + - name: Build Kotlin example + if: ${{ matrix.kit.skip_example_builds != true && matrix.kit.example_kotlin_project != '' }} + run: | + ./gradlew "${{ matrix.kit.example_kotlin_project }}:assembleDebug" \ + -c settings-kit-examples.gradle \ + -Pmparticle.kit.mparticleFromMavenLocalOnly=true + + - name: Build Java example + if: ${{ matrix.kit.skip_example_builds != true && matrix.kit.example_java_project != '' }} + run: | + ./gradlew "${{ matrix.kit.example_java_project }}:assembleDebug" \ + -c settings-kit-examples.gradle \ + -Pmparticle.kit.mparticleFromMavenLocalOnly=true diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index bfc47f71c..eb7d41c8b 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -45,12 +45,11 @@ jobs: GIT_COMMITTER_EMAIL: developers@mparticle.com steps: - name: "Clone branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} repository: mparticle/mparticle-android-sdk ref: ${{ inputs.branch_name }} - submodules: recursive - name: "Import GPG Key" uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec #v6.0 with: @@ -62,13 +61,6 @@ jobs: run: | git checkout -b regression/${{ github.run_number }} git push origin regression/${{ github.run_number }} - - name: "Commit Kit Updates" - run: | - git submodule foreach "git fetch; git reset --hard origin/main"; - git add . - git diff-index --quiet HEAD || git commit -m 'chore: Update Submodules' - - name: "Push kit updates to release branch" - run: git push origin regression/${{ github.run_number }} # instrumented-tests: # name: "Instrumented Tests" @@ -77,7 +69,7 @@ jobs: # needs: create-regression-branch # steps: # - name: "Checkout future release branch" - # uses: actions/checkout@v6.0.3 + # uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # with: # repository: mparticle/mparticle-android-sdk # ref: regression/${{ github.run_number }} @@ -110,7 +102,7 @@ jobs: # needs: create-regression-branch # steps: # - name: "Checkout Branch" - # uses: actions/checkout@v6.0.3 + # uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 # with: # repository: mparticle/mparticle-android-sdk # ref: regression/${{ github.run_number }} @@ -200,7 +192,7 @@ jobs: needs: create-regression-branch steps: - name: "Checkout future release branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: mparticle/mparticle-android-sdk ref: regression/${{ github.run_number }} @@ -229,10 +221,11 @@ jobs: needs: create-regression-branch steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: regression/${{ github.run_number }} - submodules: recursive + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -242,15 +235,15 @@ jobs: - name: "Run Android Core SDK Lint" run: ./gradlew lint - name: "Setup Android Kit Lint" - run: ./gradlew publishReleaseLocal + run: ./gradlew publishMavenPublicationToMavenLocal - name: "Run Android Kit Lint" - run: ./gradlew publishReleaseLocal -c settings-kits.gradle lint - - name: "Get SDK version for isolated kits" - id: sdk-version - run: echo "version=$(./gradlew -q properties | grep '^version:' | awk '{print $2}')" >> $GITHUB_OUTPUT + run: ./gradlew lint -c settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true - name: "Run Isolated Kit Lint (urbanairship-kit)" - working-directory: kits/urbanairship-kit - run: ./gradlew -Pversion=${{ steps.sdk-version.outputs.version }} lint + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/urbanairship/urbanairship-20 lint + - name: "Run Isolated Kit Lint (ga-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga/ga-23 lint + - name: "Run Isolated Kit Lint (ga4-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga4/ga4-23 lint - name: "Archive Test Results" uses: actions/upload-artifact@v7 if: always() @@ -271,10 +264,11 @@ jobs: needs: create-regression-branch steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: ref: regression/${{ github.run_number }} - submodules: recursive + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -284,15 +278,15 @@ jobs: - name: "Run Android Core SDK Kotlin Lint" run: ./gradlew ktlintCheck - name: "Setup Android Kit Kotlin Lint" - run: ./gradlew publishReleaseLocal + run: ./gradlew publishMavenPublicationToMavenLocal - name: "Run Android Kit Kotlin Lint" - run: ./gradlew publishReleaseLocal -c settings-kits.gradle ktlintCheck - - name: "Get SDK version for isolated kits" - id: sdk-version - run: echo "version=$(./gradlew -q properties | grep '^version:' | awk '{print $2}')" >> $GITHUB_OUTPUT + run: ./gradlew ktlintCheck -c settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true - name: "Run Isolated Kit Kotlin Lint (urbanairship-kit)" - working-directory: kits/urbanairship-kit - run: ./gradlew -Pversion=${{ steps.sdk-version.outputs.version }} ktlintCheck + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/urbanairship/urbanairship-20 ktlintCheck + - name: "Run Isolated Kit Kotlin Lint (ga-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga/ga-23 ktlintCheck + - name: "Run Isolated Kit Kotlin Lint (ga4-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga4/ga4-23 ktlintCheck - name: "Archive Test Results" uses: actions/upload-artifact@v7 if: always() @@ -317,11 +311,12 @@ jobs: GIT_COMMITTER_EMAIL: developers@mparticle.com steps: - name: "Checkout future release branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: repository: mparticle/mparticle-android-sdk ref: regression/${{ github.run_number }} - submodules: recursive + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -329,15 +324,15 @@ jobs: java-version: "17" cache: "gradle" - name: "Build Android Core" - run: ./gradlew -PisRelease=true clean publishReleaseLocal + run: ./gradlew clean publishMavenPublicationToMavenLocal - name: "Test Kits" - run: ./gradlew -PisRelease=true clean testRelease publishReleaseLocal -c settings-kits.gradle - - name: "Get SDK version for isolated kits" - id: sdk-version - run: echo "version=$(./gradlew -PisRelease=true -q properties | grep '^version:' | awk '{print $2}')" >> $GITHUB_OUTPUT + run: ./gradlew clean testRelease -c settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true - name: "Test Isolated Kits (urbanairship-kit)" - working-directory: kits/urbanairship-kit - run: ./gradlew -PisRelease=true -Pversion=${{ steps.sdk-version.outputs.version }} clean testRelease publishReleaseLocal + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/urbanairship/urbanairship-20 clean testRelease + - name: "Test Isolated Kits (ga-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga/ga-23 clean testRelease + - name: "Test Isolated Kits (ga4-kit)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga4/ga4-23 clean testRelease semantic-release-dryrun: name: "Test Semantic Release - Dry Run" @@ -351,7 +346,7 @@ jobs: GIT_COMMITTER_EMAIL: developers@mparticle.com steps: - name: "Checkout public main branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 ref: main @@ -382,7 +377,7 @@ jobs: GIT_COMMITTER_EMAIL: developers@mparticle.com steps: - name: "Checkout repo" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} fetch-depth: 0 diff --git a/.github/workflows/instrumented-tests.yml b/.github/workflows/instrumented-tests.yml new file mode 100644 index 000000000..1d6e3bbd5 --- /dev/null +++ b/.github/workflows/instrumented-tests.yml @@ -0,0 +1,69 @@ +name: Instrumented Tests (Reusable) + +on: + workflow_call: + inputs: + display_name: + required: true + type: string + gradle_command: + required: true + type: string + artifact_name: + required: true + type: string + artifact_path: + required: true + type: string + timeout_minutes: + required: false + type: number + default: 20 + +permissions: + contents: read + pull-requests: read + checks: write + +jobs: + instrumented: + name: "Instrumented Tests (${{ inputs.display_name }})" + timeout-minutes: ${{ inputs.timeout_minutes }} + runs-on: ubuntu-latest + steps: + - name: "Checkout Branch" + uses: actions/checkout@v6.0.2 + - name: "Install JDK 17" + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "17" + - name: Gradle cache + uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a #v2.9.0 + # AVD cache disabled: causes indefinite hangs on emulator shutdown (see + # ReactiveCircus/android-emulator-runner#373, #385, #362; WordPress/gutenberg#66771) + - name: Enable KVM group perms + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: "Run Instrumented Tests (${{ inputs.display_name }})" + uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b #v2.35.0 + env: + ANDROID_EMULATOR_WAIT_TIME_BEFORE_KILL: 60 + with: + api-level: 28 + force-avd-creation: false + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none + disable-animations: true + script: | + adb shell settings put global transition_animation_scale 0.0 + adb shell settings put global animator_duration_scale 0.0 + for pkg in com.mparticle.kittests com.mparticle.kits.test com.mparticle.legacyTest com.mparticle.test; do if adb shell pm list packages | grep -q "$pkg"; then adb uninstall "$pkg"; fi; done + ./gradlew ${{ inputs.gradle_command }} + - name: "Archive ${{ inputs.display_name }} Results" + uses: actions/upload-artifact@v7 + if: always() + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.artifact_path }} diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 2628f2e98..95135f56a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v5.0.0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: Trunk Check uses: trunk-io/trunk-action@04ba50e7658c81db7356da96657e6e77f220bfa3 # v1.3.1 with: @@ -28,134 +28,48 @@ jobs: name: "Check PR for hardcoded secrets" uses: mParticle/mparticle-workflows/.github/workflows/security-hardcoded-secrets.yml@main - instrumented-tests: - name: "Instrumented Tests" - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 - - name: "Install JDK 17" - uses: actions/setup-java@v5 - with: - distribution: "zulu" - java-version: "17" - - name: Gradle cache - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a #v2.9.0 - - name: AVD cache - uses: actions/cache@v5 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-29 - - name: create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b #v2.35.0 - with: - api-level: 28 - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching." - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: "Run Instrumented Tests" - uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b #v2.35.0 - with: - api-level: 28 - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: | - # Disable animations and clean up test packages - adb shell settings put global transition_animation_scale 0.0 - adb shell settings put global animator_duration_scale 0.0 - - for pkg in com.mparticle.kittests com.mparticle.kits.test com.mparticle.legacyTest com.mparticle.test; do if adb shell pm list packages | grep -q "$pkg"; then adb uninstall "$pkg"; fi; done - - ./gradlew connectedCheck --info --stacktrace - - ./gradlew :android-core:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none --info - ./gradlew :android-kit-base:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none --info - - name: "Archive Instrumented Tests Results" - uses: actions/upload-artifact@v7 - if: always() - with: - name: "instrumented-tests-results" - path: android-core/build/reports/androidTests/connected/** - - instrumented-orchestrator-tests: - name: "Instrumented Orchestrator Tests" - timeout-minutes: 30 - runs-on: ubuntu-latest - steps: - - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 - - name: "Install JDK 17" - uses: actions/setup-java@v5 - with: - distribution: "zulu" - java-version: "17" - - name: Gradle cache - uses: gradle/gradle-build-action@842c587ad8aa4c68eeba24c396e15af4c2e9f30a #v2.9.0 - - name: AVD cache - uses: actions/cache@v5 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - key: avd-29 - - - name: create AVD and generate snapshot for caching - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b #v2.35.0 - with: - api-level: 28 - force-avd-creation: false - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: false - script: echo "Generated AVD snapshot for caching." - - name: Enable KVM group perms - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - name: "Run Instrumented Orchestrator Tests" - uses: reactivecircus/android-emulator-runner@b530d96654c385303d652368551fb075bc2f0b6b #v2.35.0 - with: - api-level: 28 - force-avd-creation: false - emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none - disable-animations: true - script: | - # Disable benchmark tests as they do not work on emulators - # Disable animations and clean up test packages - adb shell settings put global transition_animation_scale 0.0 - adb shell settings put global animator_duration_scale 0.0 + instrumented-core: + uses: ./.github/workflows/instrumented-tests.yml + with: + display_name: Core + gradle_command: ":android-core:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none --info --stacktrace" + artifact_name: instrumented-core-results + artifact_path: android-core/build/reports/androidTests/connected/** + + instrumented-kit-base: + uses: ./.github/workflows/instrumented-tests.yml + strategy: + fail-fast: false + matrix: + shard: [0, 1, 2, 3] + with: + display_name: "Kit Base Shard ${{ matrix.shard }}/4" + gradle_command: ":android-kit-base:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none -Pandroid.testInstrumentationRunnerArguments.numShards=4 -Pandroid.testInstrumentationRunnerArguments.shardIndex=${{ matrix.shard }}" + artifact_name: "instrumented-kit-base-results-shard-${{ matrix.shard }}" + artifact_path: android-kit-base/build/reports/androidTests/connected/** - for pkg in com.mparticle.kittests com.mparticle.kits.test com.mparticle.legacyTest com.mparticle.test; do if adb shell pm list packages | grep -q "$pkg"; then adb uninstall "$pkg"; fi; done + instrumented-testutils: + uses: ./.github/workflows/instrumented-tests.yml + with: + display_name: Testutils + gradle_command: ":testutils:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none --info --stacktrace" + artifact_name: instrumented-testutils-results + artifact_path: testutils/build/reports/androidTests/connected/** - ./gradlew connectedCheck --stacktrace - ./gradlew -Porchestrator=true :android-core:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none - - name: "Archive Instrumented Orchestrator Tests Results" - uses: actions/upload-artifact@v7 - if: always() - with: - name: "instrumented-orchestrator-tests-results" - path: android-core/build/orchestrator/** + instrumented-orchestrator: + uses: ./.github/workflows/instrumented-tests.yml + with: + display_name: Orchestrator + gradle_command: "-Porchestrator=true :android-core:cAT -Pandroid.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=none --stacktrace" + artifact_name: instrumented-orchestrator-results + artifact_path: android-core/build/orchestrator/** unit-tests: name: "Unit Tests" timeout-minutes: 15 runs-on: ubuntu-latest steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -175,13 +89,13 @@ jobs: lint-checks: name: "Lint Checks" - timeout-minutes: 15 + timeout-minutes: 20 runs-on: macos-latest steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 - with: - submodules: recursive + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -189,6 +103,10 @@ jobs: java-version: "17" - name: "Run Android Core SDK Lint" run: ./gradlew lint + - name: "Setup Android Kit Lint" + run: ./gradlew publishMavenPublicationToMavenLocal + - name: "Run Android Kit Lint" + run: ./gradlew -c settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true lint - name: "Archive Lint Test Results" uses: actions/upload-artifact@v7 if: always() @@ -202,9 +120,9 @@ jobs: runs-on: macos-latest steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 - with: - submodules: recursive + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: @@ -212,6 +130,10 @@ jobs: java-version: "17" - name: "Run Android Core SDK Kotlin Lint" run: ./gradlew ktlintCheck + - name: "Setup Android Kit Kotlin Lint" + run: ./gradlew publishMavenPublicationToMavenLocal + - name: "Run Android Kit Kotlin Lint" + run: ./gradlew -c settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true ktlintCheck - name: "Archive Kotlin Lint Test Results" uses: actions/upload-artifact@v7 if: always() @@ -225,46 +147,49 @@ jobs: with: base_branch: main + build-kits: + uses: ./.github/workflows/build-kits.yml + kit-compatibility-test: name: "Kit Compatibility Test" runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: "Checkout Branch" - uses: actions/checkout@v6.0.3 - with: - submodules: recursive - fetch-depth: 0 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - run: echo "ORG_GRADLE_PROJECT_VERSION=$(head -n 1 VERSION)" >> $GITHUB_ENV + - run: echo "ORG_GRADLE_PROJECT_version=$(head -n 1 VERSION)" >> $GITHUB_ENV - name: "Install JDK 17" uses: actions/setup-java@v5 with: distribution: "zulu" java-version: "17" - - name: "Get Latest Kits" - run: git submodule foreach "git rebase main" - name: "Generate Core Release Build" - run: ./gradlew -PisRelease=true publishLocal + run: ./gradlew publishMavenPublicationToMavenLocal - name: "Run Kit-Base Release Tests and Build" - run: ./gradlew -PisRelease=true :android-kit-base:testRelease + run: ./gradlew :android-kit-base:testRelease - name: "Run Kit Release Tests and Build" - run: ./gradlew -PisRelease=true -p kits testRelease -c ../settings-kits.gradle - - name: "Get SDK version for isolated kits" - id: sdk-version - run: echo "version=$(./gradlew -PisRelease=true -q properties | grep '^version:' | awk '{print $2}')" >> $GITHUB_OUTPUT - - name: "Run Isolated Kit Compatibility Tests (urbanairship-kit)" - working-directory: kits/urbanairship-kit - run: ./gradlew -PisRelease=true -Pversion=${{ steps.sdk-version.outputs.version }} testRelease + run: ./gradlew -p kits testRelease -c ../settings-kits.gradle -Pmparticle.kit.mparticleFromMavenLocalOnly=true + - name: "Run Isolated Kit Compatibility Tests (urbanairship)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/urbanairship/urbanairship-20 -PisRelease=true testRelease + - name: "Run Isolated Kit Compatibility Tests (ga)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga/ga-23 -PisRelease=true testRelease + - name: "Run Isolated Kit Compatibility Tests (ga4)" + run: ./gradlew -Pmparticle.kit.mparticleFromMavenLocalOnly=true -p kits/ga4/ga4-23 -PisRelease=true testRelease automerge-dependabot: name: "Save PR Number for Dependabot Automerge" needs: [ - instrumented-tests, - instrumented-orchestrator-tests, + instrumented-core, + instrumented-kit-base, + instrumented-testutils, + instrumented-orchestrator, unit-tests, lint-checks, kotlin-lint-checks, kit-compatibility-test, + build-kits, ] uses: mParticle/mparticle-workflows/.github/workflows/dependabot-save-pr-number.yml@main @@ -273,7 +198,10 @@ jobs: github.event_name == 'pull_request' && github.event.pull_request.draft == false needs: - - instrumented-tests + - instrumented-core + - instrumented-kit-base + - instrumented-testutils + - instrumented-orchestrator - unit-tests - lint-checks - kotlin-lint-checks diff --git a/.github/workflows/release-draft.yml b/.github/workflows/release-draft.yml new file mode 100644 index 000000000..e8fd2955a --- /dev/null +++ b/.github/workflows/release-draft.yml @@ -0,0 +1,139 @@ +name: Release – Draft + +on: + workflow_dispatch: # checkov:skip=CKV_GHA_7 + inputs: + bump-type: + description: | + How to bump the base version (X.Y.Z). Use `none` to keep the base unchanged + and only re-qualify an existing pre-release (e.g. rc1 -> rc2). + required: true + type: choice + options: + - patch + - minor + - major + - none + qualifier: + description: | + Optional pre-release qualifier to append, without the leading hyphen + (e.g. `rc1`, `alpha2`, `beta`). Leave blank for a stable release. + Allowed characters: alphanumerics, dot, hyphen. + required: false + type: string + default: "" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: write + pull-requests: write + +jobs: + publish-draft-release: + runs-on: ubuntu-latest + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - name: Install JDK 17 + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "17" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + + - name: Validate qualifier input + env: + QUALIFIER: ${{ github.event.inputs.qualifier }} + run: | + if [ -n "$QUALIFIER" ] && ! [[ "$QUALIFIER" =~ ^[A-Za-z0-9][A-Za-z0-9.-]*$ ]]; then + echo "::error::qualifier '$QUALIFIER' is invalid. It must start with an alphanumeric character and may only contain alphanumerics, dot, and hyphen." + exit 1 + fi + + - name: Get current version + id: version-file + run: | + version_from_file=$(head -n 1 VERSION) + echo "release-version=$version_from_file" >> $GITHUB_OUTPUT + base_version="${version_from_file%%-*}" + echo "base-version=$base_version" >> $GITHUB_OUTPUT + + - name: Bump base version + id: bump-version + if: ${{ github.event.inputs.bump-type != 'none' }} + uses: actions-ecosystem/action-bump-semver@34e334551143a5301f38c830e44a22273c6ff5c5 # v1.0.0 + with: + current_version: ${{ steps.version-file.outputs.base-version }} + level: ${{ github.event.inputs.bump-type }} + + - name: Compose new version + id: compose-version + env: + CURRENT: ${{ steps.version-file.outputs.release-version }} + BUMPED: ${{ steps.bump-version.outputs.new_version }} + BASE: ${{ steps.version-file.outputs.base-version }} + BUMP_TYPE: ${{ github.event.inputs.bump-type }} + QUALIFIER: ${{ github.event.inputs.qualifier }} + run: | + if [ "$BUMP_TYPE" = "none" ]; then + new_base="$BASE" + else + new_base="$BUMPED" + fi + + if [ -n "$QUALIFIER" ]; then + new_version="${new_base}-${QUALIFIER}" + else + new_version="$new_base" + fi + + if [ "$new_version" = "$CURRENT" ]; then + echo "::error::Computed version '$new_version' is identical to the current VERSION. Pick a different bump-type or qualifier." + exit 1 + fi + + echo "new-version=$new_version" >> $GITHUB_OUTPUT + + - name: Save bumped version to file + run: | + echo "${{ steps.compose-version.outputs.new-version }}" > VERSION + + - name: Publish to Maven local (smoke test) + run: ./gradlew publishMavenPublicationToMavenLocal -PVERSION=${{ steps.compose-version.outputs.new-version }} + + - name: Generate changelog entry + id: changelog + # Pinned SHA for Semgrep (no mutable @main); bump when upgrading the action. + uses: ROKT/rokt-workflows/actions/generate-changelog@c5c93e92107c520fb8b8cf71070995abdf4c403f + with: + version: ${{ steps.compose-version.outputs.new-version }} + repo-url: https://github.com/${{ github.repository }} + changelog-path: CHANGELOG.md + exclude-types: chore,ci,test,build + kits-path: kits + + - name: Create Pull Request + uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0 + with: + token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} + commit-message: "chore: prepare release ${{ steps.compose-version.outputs.new-version }}" + branch: release-prep/${{ steps.compose-version.outputs.new-version }} + title: "Prepare release ${{ steps.compose-version.outputs.new-version }}" + base: ${{ github.ref_name }} + body: | + Preparing for release ${{ steps.compose-version.outputs.new-version }} from `${{ github.ref_name }}`. + - Bumped version to ${{ steps.compose-version.outputs.new-version }} diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 000000000..906e507a7 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,91 @@ +name: Release – Publish + +on: + push: + branches: + - main + - workstation/* + paths: + - VERSION + +permissions: + contents: write + +jobs: + setup-and-version: + runs-on: ubuntu-latest + outputs: + final_version: ${{ steps.version-file.outputs.release-version }} + is_prerelease: ${{ steps.version-file.outputs.is-prerelease }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Get current version + id: version-file + run: | + version_from_file=$(head -n 1 VERSION) + echo "release-version=$version_from_file" >> $GITHUB_OUTPUT + if [[ "$version_from_file" == *"-"* ]]; then + echo "is-prerelease=true" >> $GITHUB_OUTPUT + else + echo "is-prerelease=false" >> $GITHUB_OUTPUT + fi + + build-and-release: + needs: setup-and-version + runs-on: ubuntu-latest + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install JDK 17 + uses: actions/setup-java@v5 + with: + distribution: "zulu" + java-version: "17" + + - name: Setup Gradle + uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2 + + - name: Publish core to Maven local (required before kits can compile) + run: ./gradlew publishMavenPublicationToMavenLocal -PVERSION=${{ needs.setup-and-version.outputs.final_version }} + + - name: Publish core to Maven Central + run: ./gradlew publishMavenPublicationToMavenCentralRepository -PVERSION=${{ needs.setup-and-version.outputs.final_version }} + + - name: Publish kits to Maven Central + run: ./gradlew publishMavenPublicationToMavenCentralRepository -PVERSION=${{ needs.setup-and-version.outputs.final_version }} -c settings-kits.gradle + + # rokt-sdk-plus (com.rokt:rokt-sdk-plus) is an umbrella over the mParticle Rokt kit + the + # Rokt Payment Extension. The "Publish core to Maven local" step above already placed + # android-core/android-kit-base/android-kit-plugin in mavenLocal; we also need the Rokt + # kit there so the umbrella resolves its mParticle dependencies locally rather than racing + # Maven Central propagation. com.rokt:payment-extension (+ roktsdk) come from Central, + # released independently from the ROKT sdk-android-source repo. + - name: Publish Rokt kit to Maven local (so rokt-sdk-plus resolves it locally) + run: ./gradlew :kits:android-rokt:rokt:publishMavenPublicationToMavenLocal -PVERSION=${{ needs.setup-and-version.outputs.final_version }} -c settings-kits.gradle + + # Published under the com.rokt namespace, so this step uses the Rokt Maven Central + # credentials/signing key (separate from the com.mparticle secrets used above). + - name: Publish rokt-sdk-plus to Maven Central + env: + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ROKT_MAVEN_CENTRAL_SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.ROKT_MAVEN_CENTRAL_SIGNING_KEY_PASSWORD }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ROKT_SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ROKT_SONATYPE_NEXUS_PASSWORD }} + run: ./gradlew publishMavenPublicationToMavenCentralRepository -PVERSION=${{ needs.setup-and-version.outputs.final_version }} -c settings-rokt-sdk-plus.gradle + + - name: Create GitHub release + uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 + with: + makeLatest: ${{ needs.setup-and-version.outputs.is_prerelease != 'true' }} + prerelease: ${{ needs.setup-and-version.outputs.is_prerelease == 'true' }} + tag: ${{ needs.setup-and-version.outputs.final_version }} + body: | + Release ${{ needs.setup-and-version.outputs.final_version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index d4b0b4f19..000000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,199 +0,0 @@ -name: "Release SDK" - -# checkov:skip=CKV_GHA_7: This workflow requires dryRun parameter for release control -on: - workflow_dispatch: - inputs: - dryRun: - description: "Do a dry run to preview instead of a real release [true/false]" - required: true - default: "true" - -permissions: - contents: write - pull-requests: read - checks: write - -jobs: - # SDK release is done from public main branch. - confirm-public-repo-main-branch: - name: "Confirm release is run from public/main branch" - uses: mParticle/mparticle-workflows/.github/workflows/sdk-release-repo-branch-check.yml@main - - create-release-branch: - name: "Create Release Branch" - runs-on: ubuntu-latest - needs: confirm-public-repo-main-branch - env: - GITHUB_TOKEN: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} - GIT_AUTHOR_NAME: mparticle-automation - GIT_AUTHOR_EMAIL: developers@mparticle.com - GIT_COMMITTER_NAME: mparticle-automation - GIT_COMMITTER_EMAIL: developers@mparticle.com - steps: - - name: Checkout main branch - uses: actions/checkout@v6.0.3 - with: - token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} - repository: mparticle/mparticle-android-sdk - ref: main - submodules: recursive - - name: "Import GPG Key" - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec #v6.0 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - - name: "Create and push release branch" - run: | - git checkout -b release/${{ github.run_number }} - git push origin release/${{ github.run_number }} - - name: "Commit Kit Updates" - run: | - git submodule foreach "git fetch; git reset --hard origin/main"; - git add . - git diff-index --quiet HEAD || git commit -m 'chore: Update submodules' - - name: "Push kit updates to release branch" - run: git push origin release/${{ github.run_number }} - - regression-check: - needs: create-release-branch - name: "Run regression" - uses: mParticle/mparticle-android-sdk/.github/workflows/daily.yml@main - with: - dryRun: ${{ github.event.inputs.dryRun }} - branch_name: release/${{ github.run_number }} - secrets: inherit - - semantic-release: - name: "Semantic Release" - needs: regression-check - runs-on: macos-latest - env: - GITHUB_TOKEN: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} - GIT_AUTHOR_NAME: mparticle-automation - GIT_AUTHOR_EMAIL: developers@mparticle.com - GIT_COMMITTER_NAME: mparticle-automation - GIT_COMMITTER_EMAIL: developers@mparticle.com - steps: - - name: "Checkout public main branch" - uses: actions/checkout@v6.0.3 - with: - token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} - fetch-depth: 0 - ref: main - - name: "Import GPG Key" - uses: crazy-max/ghaction-import-gpg@e89d40939c28e39f97cf32126055eeae86ba74ec #v6.0 - with: - gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.GPG_PASSPHRASE }} - git_user_signingkey: true - git_commit_gpgsign: true - - name: "Merge release branch into main branch" - run: | - git pull origin release/${{ github.run_number }} - - name: "Semantic Release --dry-run" - if: ${{ github.event.inputs.dryRun == 'true' }} - run: | - npx \ - -p lodash \ - -p semantic-release@17 \ - -p @semantic-release/changelog@5 \ - -p @semantic-release/git@9 \ - -p @semantic-release/exec@5 \ - -p conventional-changelog-conventionalcommits@7 \ - semantic-release --dry-run - - name: "Semantic Release" - if: ${{ github.event.inputs.dryRun == 'false' }} - run: | - npx \ - -p lodash \ - -p semantic-release@17 \ - -p @semantic-release/changelog@5 \ - -p @semantic-release/git@9 \ - -p @semantic-release/exec@5 \ - -p conventional-changelog-conventionalcommits@7 \ - semantic-release - - name: "Push automated release commits to release branch" - if: ${{ github.event.inputs.dryRun == 'false' }} - run: | - git push origin HEAD:release/${{ github.run_number }} - - sonatype-release: - name: "Sonatype Release" - needs: semantic-release - runs-on: ubuntu-latest - env: - sonatypeUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - sonatypePassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - mavenSigningKeyId: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY }} - mavenSigningKeyPassword: ${{ secrets.MAVEN_CENTRAL_SIGNING_KEY_PASSWORD }} - steps: - - name: "Checkout release branch" - uses: actions/checkout@v6.0.3 - with: - repository: mparticle/mparticle-android-sdk - ref: release/${{ github.run_number }} - submodules: recursive - - name: "Install JDK 17" - uses: actions/setup-java@v5 - with: - distribution: "zulu" - java-version: "17" - - name: "Publish Core, KitManager, KitPlugin" - if: ${{ github.event.inputs.dryRun == 'false'}} - run: | - ./gradlew -PisRelease=true publishReleasePublicationToMavenRepository --stacktrace - - name: "Publish Kits" - if: ${{ github.event.inputs.dryRun == 'false'}} - # build kit-plugin locally so kits will compile, then release to sonatype - run: | - ./gradlew -PisRelease=true publishReleasePublicationToMavenLocal - ./gradlew -PisRelease=true publishReleasePublicationToMavenRepository -c settings-kits.gradle --stacktrace - - name: "Get SDK version for isolated kits" - if: ${{ github.event.inputs.dryRun == 'false'}} - id: sdk-version - run: echo "version=$(./gradlew -PisRelease=true -q properties | grep '^version:' | awk '{print $2}')" >> $GITHUB_OUTPUT - - name: "Publish Isolated Kits (urbanairship-kit)" - if: ${{ github.event.inputs.dryRun == 'false'}} - working-directory: kits/urbanairship-kit - run: ./gradlew -PisRelease=true -Pversion=${{ steps.sdk-version.outputs.version }} publishReleasePublicationToMavenRepository --stacktrace - - # Temporary workaround: https://central.sonatype.org/publish/publish-portal-ossrh-staging-api/#ensuring-deployment-visibility-in-the-central-publisher-portal - - name: Ensuring Deployment Visibility In The Central Publisher Portal - if: ${{ github.event.inputs.dryRun == 'false'}} - env: - USER: ${{ secrets.SONATYPE_NEXUS_USERNAME }} - PASS: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: | - curl -f -u "$USER:$PASS" -X POST \ - "https://ossrh-staging-api.central.sonatype.com/manual/upload/defaultRepository/com.mparticle?publishing_type=user_managed" \ - -H "Content-Type: application/json" -d '{}' \ - && echo "Success" - - sync-repository: - name: "Sync Repository" - needs: sonatype-release - runs-on: ubuntu-latest - steps: - - name: "Checkout main branch" - uses: actions/checkout@v6.0.3 - with: - fetch-depth: 0 - repository: ${{ github.repository }} - token: ${{ secrets.MP_SEMANTIC_RELEASE_BOT }} - ref: main - - name: "Merge release branch into main branch" - if: ${{ github.event.inputs.dryRun == 'false' }} - run: | - git pull origin release/${{ github.run_number }} - - name: "Push release commits to main branch" - if: ${{ github.event.inputs.dryRun == 'false'}} - run: | - git push origin HEAD:main - git push -f origin HEAD:chore/dependabot - - name: "Delete release branch" - if: ${{ github.event.inputs.dryRun == 'false' }} - run: | - git push --delete origin release/${{ github.run_number }} diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 361869aac..000000000 --- a/.gitmodules +++ /dev/null @@ -1,102 +0,0 @@ -[submodule "kits/adjust-kit"] - path = kits/adjust-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-adjust.git -[submodule "kits/adobe-kit"] - path = kits/adobe-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-adobe.git -[submodule "kits/adobemedia-kit"] - path = kits/adobemedia-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-adobe-media.git -[submodule "kits/appboy-kit"] - path = kits/appboy-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-appboy.git -[submodule "kits/appsflyer-kit"] - path = kits/appsflyer-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-appsflyer.git -[submodule "kits/apptentive-kit"] - path = kits/apptentive-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-apptentive.git -[submodule "kits/apptimize-kit"] - path = kits/apptimize-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-apptimize.git -[submodule "kits/apteligent-kit"] - path = kits/apteligent-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-apteligent.git -[submodule "kits/branch-kit"] - path = kits/branch-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-branch.git -[submodule "kits/button-kit"] - path = kits/button-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-button.git -[submodule "kits/clevertap-kit"] - path = kits/clevertap-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-clevertap.git -[submodule "kits/comscore-kit"] - path = kits/comscore-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-comscore.git -[submodule "kits/example-kit"] - path = kits/example-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-example.git -[submodule "kits/flurry-kit"] - path = kits/flurry-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-flurry.git -[submodule "kits/foresee-kit"] - path = kits/foresee-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-foresee.git -[submodule "kits/googleanalyticsfirebase-kit"] - path = kits/googleanalyticsfirebase-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-google-analytics-firebase.git -[submodule "kits/googleanalyticsfirebasega4-kit"] - path = kits/googleanalyticsfirebasega4-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-google-analytics-firebase-ga4.git -[submodule "kits/iterable-kit"] - path = kits/iterable-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-iterable.git -[submodule "kits/kochava-kit"] - path = kits/kochava-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-kochava.git -[submodule "kits/leanplum-kit"] - path = kits/leanplum-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-leanplum.git -[submodule "kits/localytics-kit"] - path = kits/localytics-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-localytics.git -[submodule "kits/onetrust-kit"] - path = kits/onetrust-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-onetrust.git -[submodule "kits/optimizely-kit"] - path = kits/optimizely-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-optimizely.git -[submodule "kits/pilgrim-kit"] - path = kits/pilgrim-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-pilgrim.git -[submodule "kits/radar-kit"] - path = kits/radar-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-radar.git -[submodule "kits/responsys-kit"] - path = kits/responsys-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-responsys.git -[submodule "kits/revealmobile-kit"] - path = kits/revealmobile-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-revealmobile.git -[submodule "kits/rokt-kit"] - path = kits/rokt-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-rokt.git -[submodule "kits/singular-kit"] - path = kits/singular-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-singular.git -[submodule "kits/skyhook-kit"] - path = kits/skyhook-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-skyhook.git -[submodule "kits/taplytics-kit"] - path = kits/taplytics-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-taplytics.git -[submodule "kits/tune-kit"] - path = kits/tune-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-tune.git -[submodule "kits/urbanairship-kit"] - path = kits/urbanairship-kit - url = git@github.com:mparticle-integrations/mparticle-android-integration-urbanairship.git -[submodule "kits/wootric-kit"] - path = kits/wootric-kit - url = git@github.com:mParticle-integrations/mparticle-android-integration-wootric.git diff --git a/.mobsf b/.mobsf index f99e343d7..a43a5b570 100644 --- a/.mobsf +++ b/.mobsf @@ -5,8 +5,17 @@ - android-kit-base/src/test/ - android-kit-base/src/androidTest/ - testutils/ + - "**/src/test/**" + - "**/example/**" + - "**/*-example/**" + - "**/SampleApplication/**" ignore-rules: + - webview_javascript_interface # Intentional: JS bridge for mParticle WebView SDK; workspace token controls access + - android_manifest_insecure_minsdk_error # Example apps; SDK minSdk 14+ per AGENTS.md + - android_manifest_allow_backup # Example apps only; acceptable for demo/debug + - android_manifest_missing_explicit_allow_backup # Kit lib manifests; allowBackup not applicable + - android_task_hijacking1 # Sample apps; acceptable for demo - hardcoded_api_key - hardcoded_username - android_kotlin_hardcoded @@ -21,3 +30,6 @@ - android_certificate_pinning - android_ssl_pinning - accept_self_signed_certificate + - android_manifest_well_known_assetlinks # Sample apps use third-party domains (e.g. Branch mp-fortune.app.link) + - android_task_hijacking1 # Sample apps; targetSdk in build.gradle + - android_task_hijacking2 # Sample apps; targetSdk in build.gradle diff --git a/.trunk/trunk.yaml b/.trunk/trunk.yaml index ece881dd8..012775cc9 100644 --- a/.trunk/trunk.yaml +++ b/.trunk/trunk.yaml @@ -27,11 +27,11 @@ runtimes: lint: enabled: - actionlint@1.7.7 - - checkov@3.2.470 + - checkov@3.2.513 - git-diff-check - ktlint@1.7.1 - - markdownlint@0.45.0 - - prettier@3.6.2 + - markdownlint@0.48.0 + - prettier@3.8.1 - shellcheck@0.11.0 - shfmt@3.6.0 - trufflehog@3.90.6 @@ -65,8 +65,9 @@ lint: ignore: - linters: [ALL] paths: - - gradlew + - "**/gradlew" - scripts/install-start-emulator.sh + - kits/urbanairship/urbanairship-20/** actions: enabled: - trunk-announce diff --git a/CHANGELOG.md b/CHANGELOG.md index adbf7e5d7..9949b0890 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,18 +1,136 @@ -## [5.80.0](https://github.com/mParticle/mparticle-android-sdk/compare/v5.79.2...v5.80.0) (2026-06-25) + -### Features +## [Unreleased] -- add device-based consent to override MPID-scoped consent ([#726](https://github.com/mParticle/mparticle-android-sdk/issues/726)) ([e92d352](https://github.com/mParticle/mparticle-android-sdk/commit/e92d3522a350be90a1f15eb3b81d5e6f089f1aed)) +### Added -## Unreleased +- Add Rokt Shoppable Ads payment extension registration and selection APIs. +- Add the `com.rokt:rokt-sdk-plus` umbrella artifact: a single dependency bundling the mParticle core SDK, the mParticle Rokt kit, and the Rokt Payment Extension (Shoppable Ads), mirroring the iOS `RoktSDKPlus` umbrella. -### Fixed +### Changed -- Restore async wait for user attributes to persist before Rokt `selectPlacements` delegates to the kit, fixing a regression where placement attributes could be missing in mParticle and Rokt. +- Upgrade Kotlin to 2.1.20 across the core SDK, buildSrc, and all integrated kits. +- Add support for qualified alpha, beta, and release candidate versions in release workflows. +- Add Kotlin `MParticle.rokt` access and `RoktLayout` event callbacks for the Rokt kit. +- Centralize the Rokt SDK / payment extension versions in root `gradle.properties` (`roktSdkVersion`, `roktPaymentExtensionVersion`), shared by the Rokt kit and `rokt-sdk-plus`. -### Added +### Removed + +- Remove deprecated `KitIntegration.getAllUserAttributes()`. Custom kits must use `getCurrentUser().getUserAttributes()` (or other `FilteredMParticleUser` APIs) and `AttributeListener` callbacks instead ([#682](https://github.com/mParticle/mparticle-android-sdk/pull/682)) +- Remove deprecated `KitIntegration.getUserIdentities()`. Custom kits must use identity data from kit callbacks and request objects instead ([#681](https://github.com/mParticle/mparticle-android-sdk/pull/681)) ([8d3a23c8](https://github.com/mParticle/mparticle-android-sdk/commit/8d3a23c84c96d11f0ee1f80763adacc4f964b544)) + +## [6.0.0-rc.1] - 2026-05-22 + +### Core + +#### Added + +- Split instrumented tests in CI for parallel execution ([#662](https://github.com/mParticle/mparticle-android-sdk/pull/662)) + +#### Changed + +- Centralize active kit filtering in KitManagerImpl ([#697](https://github.com/mParticle/mparticle-android-sdk/pull/697)) + +### Kits + +#### Kits + +##### Added + +- Migrate OneTrust kit ([#672](https://github.com/mParticle/mparticle-android-sdk/pull/672)) +- Migrate Optimizely kit ([#664](https://github.com/mParticle/mparticle-android-sdk/pull/664)) +- Migrate Singular 12 kit ([#667](https://github.com/mParticle/mparticle-android-sdk/pull/667)) +- Migrate Radar kit ([#665](https://github.com/mParticle/mparticle-android-sdk/pull/665)) +- Migrate ComScore 6 kit ([#668](https://github.com/mParticle/mparticle-android-sdk/pull/668)) +- Migrate Iterable 3 kit ([#669](https://github.com/mParticle/mparticle-android-sdk/pull/669)) +- Migrate Adobe 5 kit ([#670](https://github.com/mParticle/mparticle-android-sdk/pull/670)) +- Migrate Adobe Media 5 kit ([#671](https://github.com/mParticle/mparticle-android-sdk/pull/671)) +- Migrate Apptimize kit ([#658](https://github.com/mParticle/mparticle-android-sdk/pull/658)) +- Migrate Apptentive kit ([#657](https://github.com/mParticle/mparticle-android-sdk/pull/657)) +- Migrate CleverTap kit ([#660](https://github.com/mParticle/mparticle-android-sdk/pull/660)) +- Migrate Localytics kit ([#661](https://github.com/mParticle/mparticle-android-sdk/pull/661)) +- Migrate Adjust kit ([#656](https://github.com/mParticle/mparticle-android-sdk/pull/656)) +- Kochava 5 Kit Migrate ([#654](https://github.com/mParticle/mparticle-android-sdk/pull/654)) +- Migrate Rokt kit ([#659](https://github.com/mParticle/mparticle-android-sdk/pull/659)) +- Braze 38 Kit Migrate ([#651](https://github.com/mParticle/mparticle-android-sdk/pull/651)) +- Migrate appsflyer-kit from submodule to regular directory ([#650](https://github.com/mParticle/mparticle-android-sdk/pull/650)) + +#### Adjust + +##### Fixed + +- Resolve trunk issues for all kits ([#675](https://github.com/mParticle/mparticle-android-sdk/pull/675)) + +#### Adobe + +##### Added + +- Move onSetUserAttributeList to BaseAttributeListener ([#690](https://github.com/mParticle/mparticle-android-sdk/pull/690)) +- Migrate setUserAttribute ([#689](https://github.com/mParticle/mparticle-android-sdk/pull/689)) +- Inline user attribute removal in onRemoveUserAttribute ([#687](https://github.com/mParticle/mparticle-android-sdk/pull/687)) +- Split LogoutListener from AttributeListener ([#685](https://github.com/mParticle/mparticle-android-sdk/pull/685)) + +##### Changed + +- KitManagerImpl user-attribute forwarding and UserAttributeListener cleanup ([#693](https://github.com/mParticle/mparticle-android-sdk/pull/693)) +- Rename AttributeListener to ModifyIdentityListener ([#692](https://github.com/mParticle/mparticle-android-sdk/pull/692)) +- Route bulk user attribute sync through onSetAllUserAttributes ([#691](https://github.com/mParticle/mparticle-android-sdk/pull/691)) + +#### Apptentive + +##### Added -- Add device-based consent APIs (`MParticle.getDeviceConsentState()`, `MParticle.setDeviceConsentState()`) and `MParticleOptions.Builder.deviceBasedConsentEnabled()` so consent can be stored and applied at the device level, overriding MPID-based consent for kit forwarding rules and event uploads. +- Migrate Branch 5 kit ([#666](https://github.com/mParticle/mparticle-android-sdk/pull/666)) + +#### Branch + +##### Added + +- Removing deprecations - Part 1 ([#676](https://github.com/mParticle/mparticle-android-sdk/pull/676)) + +#### Braze + +##### Added + +- Add Braze SDK 40 kit (braze-40) ([#679](https://github.com/mParticle/mparticle-android-sdk/pull/679)) +- Add Braze SDK 41 kit (braze-41) ([#680](https://github.com/mParticle/mparticle-android-sdk/pull/680)) +- Add Braze 39 kit ([#677](https://github.com/mParticle/mparticle-android-sdk/pull/677)) +- Leanplum 7 Kit Migrate ([#655](https://github.com/mParticle/mparticle-android-sdk/pull/655)) + +#### Ga + +##### Added + +- GA 23 Kit Migrate ([#652](https://github.com/mParticle/mparticle-android-sdk/pull/652)) + +#### Ga4 + +##### Added + +- GA4 23 Kit Migrate ([#653](https://github.com/mParticle/mparticle-android-sdk/pull/653)) + +#### Iterable + +##### Added + +- Migrate Urban Airship kit ([#673](https://github.com/mParticle/mparticle-android-sdk/pull/673)) + +#### Rokt + +##### Added + +- Add customBaseURL CNAME support to NetworkOptions ([#702](https://github.com/mParticle/mparticle-android-sdk/pull/702)) +- Migrate Rokt kit to Rokt SDK 5.x and remove MpRoktEventCallback ([#704](https://github.com/mParticle/mparticle-android-sdk/pull/704)) + +##### Changed + +- Migrate Rokt contracts and facade ownership to rokt-kit ([#700](https://github.com/mParticle/mparticle-android-sdk/pull/700)) + +## [5.80.0](https://github.com/mParticle/mparticle-android-sdk/compare/v5.79.2...v5.80.0) (2026-06-25) + +### Features + +- add device-based consent to override MPID-scoped consent ([#726](https://github.com/mParticle/mparticle-android-sdk/issues/726)) ([e92d352](https://github.com/mParticle/mparticle-android-sdk/commit/e92d3522a350be90a1f15eb3b81d5e6f089f1aed)) ## [5.79.2](https://github.com/mParticle/mparticle-android-sdk/compare/v5.79.1...v5.79.2) (2026-06-22) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 075b2a8aa..22c05d828 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,7 +85,7 @@ for details. Before submitting a PR that affects core APIs, verify isolated kits also build: ```bash -cd kits/urbanairship-kit && ./gradlew testRelease +cd kits/urbanairship/urbanairship-20 && ./gradlew testRelease ``` ### Reporting Bugs diff --git a/MIGRATING.md b/MIGRATING.md new file mode 100644 index 000000000..14fc0eb21 --- /dev/null +++ b/MIGRATING.md @@ -0,0 +1,399 @@ +# Migration Guides + +## Migrating from versions < 6.0.0 + +This guide covers the breaking changes introduced in mParticle Android SDK 6.0.0 and how to migrate your code to the new APIs. + +--- + +### Removed `isAndroidIdDisabled()` / `androidIdDisabled(boolean)` + +The `isAndroidIdDisabled()` method on `MParticle` and `MParticleOptions`, and the `androidIdDisabled(boolean)` builder method on `MParticleOptions.Builder`, have been removed. These were replaced by inverted equivalents with clearer semantics. + +Replace `isAndroidIdDisabled()` with `isAndroidIdEnabled()` and invert the logic: + +Java: + +```java +// Before +if (MParticle.isAndroidIdDisabled()) { ... } +if (options.isAndroidIdDisabled()) { ... } + +// After +if (!MParticle.isAndroidIdEnabled()) { ... } +if (!options.isAndroidIdEnabled()) { ... } +``` + +Kotlin: + +```kotlin +// Before +if (MParticle.isAndroidIdDisabled()) { ... } +if (options.isAndroidIdDisabled()) { ... } + +// After +if (!MParticle.isAndroidIdEnabled()) { ... } +if (!options.isAndroidIdEnabled()) { ... } +``` + +Replace `androidIdDisabled(boolean)` with `androidIdEnabled(boolean)` and invert the value: + +Java: + +```java +// Before +MParticleOptions.builder(context) + .androidIdDisabled(true) + .build(); + +// After +MParticleOptions.builder(context) + .androidIdEnabled(false) + .build(); +``` + +Kotlin: + +```kotlin +// Before +MParticleOptions.builder(context) + .androidIdDisabled(true) + .build() + +// After +MParticleOptions.builder(context) + .androidIdEnabled(false) + .build() +``` + +--- + +### Removed `isAutoTrackingEnabled()` + +The `isAutoTrackingEnabled()` method on `MParticle` has been removed. This method always returned `false` and automatic screen tracking via activity lifecycle hooks has been removed. + +Remove any calls to `isAutoTrackingEnabled()`. If your code depends on this value, note that it was always `false` — no screen tracking was being performed automatically. + +Java: + +```java +// Before +if (MParticle.getInstance().isAutoTrackingEnabled()) { + // this block was never executed +} + +// After +// Remove the call entirely +``` + +Kotlin: + +```kotlin +// Before +if (MParticle.getInstance()?.isAutoTrackingEnabled == true) { + // this block was never executed +} + +// After +// Remove the call entirely +``` + +--- + +### Removed `isProviderActive(int)` + +The `isProviderActive(int)` method on `MParticle` has been removed. It has been renamed to `isKitActive(int)`. + +Replace `isProviderActive(serviceProviderId)` with `isKitActive(serviceProviderId)`: + +Java: + +```java +// Before +boolean active = MParticle.getInstance().isProviderActive(MParticle.ServiceProviders.APPBOY); + +// After +boolean active = MParticle.getInstance().isKitActive(MParticle.ServiceProviders.APPBOY); +``` + +Kotlin: + +```kotlin +// Before +val active = MParticle.getInstance()?.isProviderActive(MParticle.ServiceProviders.APPBOY) + +// After +val active = MParticle.getInstance()?.isKitActive(MParticle.ServiceProviders.APPBOY) +``` + +--- + +### Removed `MPEvent.setInfo()` / `getInfo()` / `Builder.info()` + +The `setInfo(Map)`, `getInfo()`, and `Builder.info(Map)` methods on `MPEvent` have been removed. They have been renamed to `setCustomAttributes()`, `getCustomAttributes()`, and `Builder.customAttributes()`. + +Replace usages with the new method names: + +Java: + +```java +// Before +MPEvent event = new MPEvent.Builder("Event Name", MParticle.EventType.Other) + .info(attributes) + .build(); +Map info = event.getInfo(); +event.setInfo(attributes); + +// After +MPEvent event = new MPEvent.Builder("Event Name", MParticle.EventType.Other) + .customAttributes(attributes) + .build(); +Map customAttributes = event.getCustomAttributes(); +event.setCustomAttributes(attributes); +``` + +Kotlin: + +```kotlin +// Before +val event = MPEvent.Builder("Event Name", MParticle.EventType.Other) + .info(attributes) + .build() +val info = event.getInfo() +event.setInfo(attributes) + +// After +val event = MPEvent.Builder("Event Name", MParticle.EventType.Other) + .customAttributes(attributes) + .build() +val customAttributes = event.getCustomAttributes() +event.setCustomAttributes(attributes) +``` + +--- + +### Removed `UserAttributeListener` + +The `com.mparticle.UserAttributeListener` interface has been removed. It has been replaced by `com.mparticle.TypedUserAttributeListener`. + +The key difference is that `onUserAttributesReceived` now receives `Map` instead of `Map` for user attribute singles, allowing typed values (numbers, booleans) to be preserved rather than stringified. Additionally, the `mpid` parameter is non-nullable (`Long` instead of `Long?`). + +Java: + +```java +// Before +user.getUserAttributes(new UserAttributeListener() { + @Override + public void onUserAttributesReceived( + @Nullable Map userAttributes, + @Nullable Map> userAttributeLists, + @Nullable Long mpid + ) { + // handle attributes + } +}); + +// After +user.getUserAttributes(new TypedUserAttributeListener() { + @Override + public void onUserAttributesReceived( + @NonNull Map userAttributes, + @NonNull Map> userAttributeLists, + long mpid + ) { + // handle attributes — values may be String, Number, or null + } +}); +``` + +Kotlin: + +```kotlin +// Before +user.getUserAttributes(UserAttributeListener { userAttributes, userAttributeLists, mpid -> + // userAttributes: Map? +}) + +// After +user.getUserAttributes(object : TypedUserAttributeListener { + override fun onUserAttributesReceived( + userAttributes: Map, + userAttributeLists: Map?>, + mpid: Long + ) { + // userAttributes values may be String, Number, Boolean, or null + } +}) +``` + +Note: numeric and boolean attribute values are no longer automatically converted to strings. If you need string values, call `.toString()` on each value manually. + +--- + +### Removed `UserAliasHandler` / `IdentityApiRequest.Builder.userAliasHandler()` + +The `com.mparticle.identity.UserAliasHandler` interface and the `IdentityApiRequest.Builder.userAliasHandler()` method have been removed. Use a success listener on the `BaseIdentityTask` returned by identity API calls instead. + +Java: + +```java +// Before +IdentityApiRequest request = IdentityApiRequest.withEmptyUser() + .email("user@example.com") + .userAliasHandler(new UserAliasHandler() { + @Override + public void onUserAlias(MParticleUser previousUser, MParticleUser newUser) { + // copy attributes from previousUser to newUser + } + }) + .build(); +MParticle.getInstance().Identity().login(request); + +// After +IdentityApiRequest request = IdentityApiRequest.withEmptyUser() + .email("user@example.com") + .build(); +MParticle.getInstance().Identity().login(request) + .addSuccessListener(new TaskSuccessListener() { + @Override + public void onSuccess(IdentityApiResult result) { + MParticleUser newUser = result.getUser(); + MParticleUser previousUser = result.getPreviousUser(); + if (previousUser != null) { + // copy attributes from previousUser to newUser + } + } + }); +``` + +Kotlin: + +```kotlin +// Before +val request = IdentityApiRequest.withEmptyUser() + .email("user@example.com") + .userAliasHandler { previousUser, newUser -> + // copy attributes from previousUser to newUser + } + .build() +MParticle.getInstance()?.Identity()?.login(request) + +// After +val request = IdentityApiRequest.withEmptyUser() + .email("user@example.com") + .build() +MParticle.getInstance()?.Identity()?.login(request) + ?.addSuccessListener { result -> + val newUser = result.user + val previousUser = result.previousUser + // copy attributes from previousUser to newUser + } +``` + +--- + +### Removed `ConsentState.Builder.setCCPAConsent()` / `removeCCPAConsent()` + +The `setCCPAConsent(CCPAConsent)` and `removeCCPAConsent()` methods on `ConsentState.Builder` have been removed. They have been renamed to `setCCPAConsentState(CCPAConsent)` and `removeCCPAConsentState()`. + +Java: + +```java +// Before +ConsentState state = ConsentState.builder() + .setCCPAConsent(CCPAConsent.builder(true).build()) + .build(); + +ConsentState.builder().removeCCPAConsent(); + +// After +ConsentState state = ConsentState.builder() + .setCCPAConsentState(CCPAConsent.builder(true).build()) + .build(); + +ConsentState.builder().removeCCPAConsentState(); +``` + +Kotlin: + +```kotlin +// Before +val state = ConsentState.builder() + .setCCPAConsent(CCPAConsent.builder(true).build()) + .build() + +ConsentState.builder().removeCCPAConsent() + +// After +val state = ConsentState.builder() + .setCCPAConsentState(CCPAConsent.builder(true).build()) + .build() + +ConsentState.builder().removeCCPAConsentState() +``` + +--- + +### Rokt API moved from `android-core` to `rokt-kit` + +Rokt APIs are now owned by the Rokt kit module. Usage remains the same (`MParticle.getInstance().Rokt()`); update imports to use the kit package types. + +Kotlin: + +```kotlin +// Before +import com.mparticle.Rokt + +val rokt = MParticle.getInstance()?.Rokt() + +// After +import com.mparticle.kits.Rokt + +val rokt = MParticle.getInstance()?.Rokt() +``` + +Java: + +```java +// Before +com.mparticle.Rokt rokt = MParticle.getInstance().Rokt(); + +// After +com.mparticle.kits.Rokt rokt = MParticle.getInstance().Rokt(); +``` + +--- + +### Rokt wrapper types replaced with native Rokt SDK types + +The following mParticle wrapper types were removed: + +- `com.mparticle.RoktEvent` +- `com.mparticle.MpRoktEventCallback` +- `com.mparticle.rokt.RoktConfig` +- `com.mparticle.rokt.PlacementOptions` + +Use native Rokt SDK types instead: + +- `com.rokt.roktsdk.RoktEvent` +- `com.rokt.roktsdk.Rokt.RoktCallback` +- `com.rokt.roktsdk.RoktConfig` +- `com.rokt.roktsdk.PlacementOptions` + +Kotlin: + +```kotlin +// Before +val callback = object : MpRoktEventCallback { /* ... */ } +val config = com.mparticle.rokt.RoktConfig.Builder().build() + +// After +val callback = object : com.rokt.roktsdk.Rokt.RoktCallback { /* ... */ } +val config = com.rokt.roktsdk.RoktConfig.Builder().build() +``` + +--- + +### `Rokt.prepareAttributesAsync(...)` is no longer public API + +`prepareAttributesAsync` is now internal to the kit implementation and should not be called directly from app code. diff --git a/ONBOARDING.md b/ONBOARDING.md index 4ac427a6f..a45cb045f 100644 --- a/ONBOARDING.md +++ b/ONBOARDING.md @@ -69,16 +69,10 @@ version in the **Gradle JDK** dropdown menu. Remember to close and reopen your project after you've made your changes or the error message may persist. -### Downloading the mParticle Kits +### mParticle Kits -Now we will download the different available Kits which are developed as individual submodules each -having a dedicated repository. First make sure you have set both a SSH key and GPG key in your -GitHub account, SSH is used for authentication while GPG is used for signing tags and commits. - -Now type the following commands `git submodule init` (creates the local configuration file for the -submodules). followed by `git submodule update --recursive` (updates all available submodules). - -Or you can do it in one line with `git submodule update --init` +All kits are included in the monorepo under the `kits/` directory. Clone the repository and you +will have the full SDK with all available kits. No submodule initialization is required. An error regarding the Javadoc file generation, **Illegal Package Name** may come up referencing some files with the .kt extension, a current quick fix for this is adding the following code in @@ -90,8 +84,7 @@ tasks.withType(Javadoc) { } ``` -Once you have run both commands you should see your newly populated kit files easily in the Project -view -> **Kits** folder. +Kit files are located in the Project view -> **Kits** folder. ### Building and publishing core modules to the Maven Local Repository @@ -148,12 +141,14 @@ Kotlin version. **Currently isolated:** -- `urbanairship-kit` (Kotlin 2.2.x, `urbanairship-core:20.3.0`) +- `kits/urbanairship/urbanairship-20` (Kotlin 2.2.x, `urbanairship-core:20.3.0`) +- `kits/ga/ga-23` (Kotlin 2.2.x, `firebase-analytics:23.x`) +- `kits/ga4/ga4-23` (Kotlin 2.2.x, `firebase-analytics:23.x`) To build an isolated kit after publishing core to mavenLocal: ```bash -cd kits/urbanairship-kit +cd kits/urbanairship/urbanairship-20 ./gradlew testRelease publishReleaseLocal ``` @@ -165,13 +160,13 @@ To verify all kits (main + isolated): ```bash ./gradlew -PisRelease=true publishReleaseLocal ./gradlew -PisRelease=true testRelease publishReleaseLocal -c settings-kits.gradle -cd kits/urbanairship-kit && ./gradlew -PisRelease=true testRelease +cd kits/urbanairship/urbanairship-20 && ./gradlew -PisRelease=true testRelease ``` **Adding a new isolated kit:** If a kit upgrades to a Kotlin version -incompatible with the root KGP (2.0.20), remove it from +incompatible with the root KGP (2.1.20), remove it from `settings-kits.gradle` with a comment, and add standalone build steps -to the CI workflows following the urbanairship-kit pattern. +to the CI workflows following the urbanairship pattern. ## Read More diff --git a/README.md b/README.md index f6be482de..3303e90e0 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ You can grab the Core SDK via Maven Central. Please see the badge above and foll ```groovy dependencies { - implementation 'com.mparticle:android-core:5.80.0' + implementation 'com.mparticle:android-core:6.0.0-rc.1' } ``` @@ -31,8 +31,8 @@ Several integrations require additional client-side add-on libraries called "kit ```groovy dependencies { implementation ( - 'com.mparticle:android-example-kit:5.80.0', - 'com.mparticle:android-another-kit:5.80.0' + 'com.mparticle:android-example-kit:6.0.0-rc.1', + 'com.mparticle:android-another-kit:6.0.0-rc.1' ) } ``` diff --git a/RELEASE.md b/RELEASE.md index 2eafd710f..11b0971f4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,82 +2,81 @@ This document outlines the process for releasing the mParticle Android SDK and its kits. -## Step 1: Preparing the SDK for Release +The Android SDK and kits are released together from this monorepo using GitHub Actions. -The Android SDK and kits are released using GitHub Actions. The SDK and kits are currently coupled together in the release process. +## Release workflows -### Pre-release Checklist +### Release - Draft -- Ensure all commits are in the public main branch -- Review `release.yml` in the repo for specific workflow details -- The release job deploys the most current snapshot of main branch release tag to main branch +Use `Release - Draft` to prepare a release PR. The workflow: -## Step 2: Release via GitHub Actions +1. Reads the current version from `VERSION`. +2. Computes the next release version from `bump-type` and optional `qualifier`. +3. Writes the computed version back to `VERSION`. +4. Publishes to Maven local as a smoke test. +5. Generates the changelog entry. +6. Opens a PR back into the branch selected in the workflow dispatch UI. -### What the GitHub Release Job Does +Generated release PR branches use `release/prep-`, for example `release/prep-6.0.0-rc1`. -1. **Initial Setup** - - Verifies job is running from public repo and on main branch - - Creates temporary `release/{run_number}` branch +### Release - Publish -2. **Testing Phase** - - Runs unit and instrumented tests in parallel - - Instrumented tests require an emulator - - Unit tests run independently - - Updates kits and runs additional tests +`Release - Publish` runs when `VERSION` changes on: -3. **Version Management** - - Runs semantic version action - - Automatically bumps `build.gradle` version based on commit messages - - No version bump if no new commits (e.g., feat/fix) - - Generates release notes automatically - - Requires linear history between development and main branches +- `main` +- `workstation/*` -4. **Artifact Publishing** - - Uploads artifacts to Sonatype (core and kits) - - Builds and signs the core SDK and all kit artifacts - - Uploads to Sonatype Nexus (staging area) - - Syncs artifacts to Maven Central - > Note: This step will be moved before version bump during semantic release +The workflow publishes the SDK and kits to Maven Central using the exact version in `VERSION`, then creates a GitHub release with the same tag. -5. **Branch Synchronization** - - Pushes release branch to: - - Public main branch - - Public development branch - - Internal repo main branch - - Deletes release branch on success (preserved on failure for debugging) +## Stable releases -### How to Release +1. Run `Release - Draft` from the target branch. +2. Choose `patch`, `minor`, or `major` for `bump-type`. +3. Leave `qualifier` empty. +4. Review, approve, and merge the generated release PR. +5. Confirm `Release - Publish` succeeds. -1. Navigate to the Actions tab -2. Select "release SDK" -3. Run the workflow from main branch with "true" first to perform a dry run - > Important: Always start with a dry run to validate the release process. This will perform all steps up to semantic release without actually publishing, helping catch potential issues early. -4. If the dry run succeeds, run the workflow again with "false" option to perform the actual release - > Note: Only proceed with the actual release after confirming a successful dry run +Stable releases are marked as the latest GitHub release. -### Important Notes +## Alpha, beta, and RC releases -- **Release Duration**: Expect ~20 minutes due to comprehensive test suite -- **Emulator Issues**: - - Sometimes GitHub Actions emulators fail - - We have a custom script to install and start the emulator `scripts/install-start-emulator.sh` - - OS version is hardcoded to avoid issues with new releases -- **Code Reusability**: - - Reusable GitHub Actions are defined in the [mparticle-workflows repo](https://github.com/mParticle/mparticle-workflows) - - This enables other platforms to reuse similar jobs +Use a qualified release when a partner or internal validation flow needs a fixed Maven Central version before the stable release, for example `6.0.0-alpha1`, `6.0.0-beta1`, or `6.0.0-rc1`. -## Post-Release Verification +1. Run `Release - Draft` from the target branch, such as `workstation/6.0-Release`. +2. Choose the base `bump-type`: + - `patch`, `minor`, or `major` to bump the base version. + - `none` to keep the base version unchanged and only change or remove the qualifier. +3. Set `qualifier` to the pre-release identifier without the leading hyphen, for example `alpha1`, `beta1`, or `rc1`. +4. Review, approve, and merge the generated release PR. +5. Confirm `Release - Publish` succeeds. -After a successful build through GitHub Actions, verify: +Qualified releases are published to Maven Central with the exact qualified version. Their GitHub releases are marked as pre-releases and are not marked as latest. -1. Public repo has a new semantic release tag -2. New artifact is present in [Sonatype](https://central.sonatype.com/publishing) +The qualifier must start with an alphanumeric character and may only contain alphanumerics, dots, and hyphens. + +## Version examples + +- `5.78.2` with `bump-type=minor` and `qualifier=alpha1` produces `5.79.0-alpha1`. +- `5.79.0-alpha1` with `bump-type=none` and `qualifier=beta1` produces `5.79.0-beta1`. +- `5.79.0-beta1` with `bump-type=none` and `qualifier=rc1` produces `5.79.0-rc1`. +- `5.79.0-rc1` with `bump-type=none` and an empty `qualifier` produces `5.79.0`. + +The draft workflow rejects any input combination that would produce the same version already stored in `VERSION`. + +## Post-release verification + +After a successful publish workflow, verify: + +1. The GitHub release exists with the expected tag. +2. The SDK artifact is present in [Sonatype](https://central.sonatype.com/publishing). +3. The kit artifacts are present in [Sonatype](https://central.sonatype.com/publishing). +4. Pre-release tags are marked as pre-releases and stable tags are marked as latest. ## Troubleshooting -If you encounter emulator issues during testing, check: +If release validation fails: -- [Emulator setup script](https://github.com/mParticle/mparticle-android-sdk/blob/main/scripts/install-start-emulator.sh) -- Current OS version compatibility -- GitHub Actions logs for specific error messages +- Check the generated release PR for the computed `VERSION` and changelog changes. +- Check the Maven local smoke-test step in `Release - Draft`. +- Check the Maven Central publish steps in `Release - Publish`. +- For emulator-related validation issues, check `scripts/install-start-emulator.sh` and the GitHub Actions logs. diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..c3a4d6bbb --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +6.0.0-rc.1 diff --git a/android-core/build.gradle b/android-core/build.gradle index f3230652f..518641f00 100644 --- a/android-core/build.gradle +++ b/android-core/build.gradle @@ -1,11 +1,12 @@ -ext { - kitDescription = 'Core mParticle SDK supporting only server-side integrations.' -} - apply plugin: 'com.android.library' -apply from: '../scripts/maven.gradle' +apply plugin: 'mparticle.android.library.publish' apply plugin: 'kotlin-android' +mparticleMavenPublish { + artifactId.set('android-core') + description.set('Core mParticle SDK supporting only server-side integrations.') +} + android { namespace 'com.mparticle' compileSdk 33 @@ -45,7 +46,7 @@ android { defaultConfig { minSdk 14 targetSdk 33 - versionName project.version.toString().replace('-SNAPSHOT', '') + versionName (project.findProperty("VERSION") ?: "0.0.0").toString() versionCode Integer.parseInt(new Date().format('yyyyMMdd')) buildConfigField 'String', 'GIT_SHA', "\"$gitHash\"" @@ -106,36 +107,6 @@ android { } } -task coreSdkJavadocs(type: Javadoc) { - android.libraryVariants.all { variant -> - if (variant.name == 'release') { - source = android.sourceSets.main.java.srcDirs - source += 'build/generated/source/buildConfig/release/' - owner.classpath += variant.javaCompileProvider.get().classpath - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - classpath += project.files('src/main/java') - } - title = 'mParticle Android SDK API Reference' - failOnError true - getOptions().setNoTimestamp(true) - exclude { - String filePath = it.toString() - filePath.contains('/com/mparticle/internal/') || - filePath.contains('/com/mparticle/kits/') - } - } -} - -task generateJavadocsJar(type: Jar, dependsOn: coreSdkJavadocs) { - archiveClassifier.set('javadoc') - from coreSdkJavadocs.destinationDir -} - -task generateSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - dependencies { //noinspection GradleCompatible compileOnly 'com.google.firebase:firebase-messaging:[10.2.1, )' @@ -159,8 +130,8 @@ dependencies { testImplementation project(':testutils') testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" androidTestImplementation project(':testutils') if (useOrchestrator()) { @@ -169,7 +140,7 @@ dependencies { } androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" androidTestImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version" - androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_version" + androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" } diff --git a/android-core/src/androidTest/kotlin/com.mparticle/MPUserTest.kt b/android-core/src/androidTest/kotlin/com.mparticle/MPUserTest.kt index 663adecdb..54b939850 100644 --- a/android-core/src/androidTest/kotlin/com.mparticle/MPUserTest.kt +++ b/android-core/src/androidTest/kotlin/com.mparticle/MPUserTest.kt @@ -4,7 +4,6 @@ import com.mparticle.internal.AccessUtils import com.mparticle.testutils.BaseCleanStartedEachTest import org.junit.Test import kotlin.test.assertEquals -import kotlin.test.assertNotNull import kotlin.test.assertTrue class MPUserTest : BaseCleanStartedEachTest() { @@ -34,26 +33,6 @@ class MPUserTest : BaseCleanStartedEachTest() { this.userAttributes.put("fooNull", null) android_test_hack() - getUserAttributes( - object : UserAttributeListener { - override fun onUserAttributesReceived( - userAttributes: Map?, - userAttributeLists: Map>?, - mpid: Long?, - ) { - assertNotNull(userAttributes) - assertEquals(6, userAttributes.size) - assertEquals("bar", userAttributes["foo"]) - assertEquals("123", userAttributes["fooInt"]) - assertEquals("12345", userAttributes["fooLong"]) - assertEquals("10.15", userAttributes["fooDouble"]) - assertEquals("-10", userAttributes["fooNegInt"]) - assertEquals("-1010", userAttributes["fooNegLong"]) - assertEquals(null, userAttributes["fooNull"]) - } - }, - ) - getUserAttributes( object : TypedUserAttributeListener { override fun onUserAttributesReceived( diff --git a/android-core/src/androidTest/kotlin/com.mparticle/MParticleOptionsTest.kt b/android-core/src/androidTest/kotlin/com.mparticle/MParticleOptionsTest.kt index 6249078c0..dfa3e6591 100644 --- a/android-core/src/androidTest/kotlin/com.mparticle/MParticleOptionsTest.kt +++ b/android-core/src/androidTest/kotlin/com.mparticle/MParticleOptionsTest.kt @@ -156,25 +156,12 @@ class MParticleOptionsTest : BaseAbstractTest() { @Test @Throws(Exception::class) - fun testAndroidIdDisabled() { + fun testAndroidIdEnabled() { // test defaults Assert.assertFalse(MParticle.isAndroidIdEnabled()) - Assert.assertTrue(MParticle.isAndroidIdDisabled()) MParticle.setInstance(null) startMParticle(MParticleOptions.builder(mContext)) Assert.assertFalse(MParticle.isAndroidIdEnabled()) - Assert.assertTrue(MParticle.isAndroidIdDisabled()) - - // test androidIdDisabled == true - MParticle.setInstance(null) - startMParticle( - MParticleOptions - .builder(mContext) - .androidIdDisabled(true), - ) - Assert.assertFalse(MParticle.isAndroidIdEnabled()) - Assert.assertTrue(MParticle.isAndroidIdDisabled()) - MParticle.setInstance(null) // test androidIdEnabled == false MParticle.setInstance(null) @@ -184,18 +171,8 @@ class MParticleOptionsTest : BaseAbstractTest() { .androidIdEnabled(false), ) Assert.assertFalse(MParticle.isAndroidIdEnabled()) - Assert.assertTrue(MParticle.isAndroidIdDisabled()) MParticle.setInstance(null) - // test androidIdDisabled == false - startMParticle( - MParticleOptions - .builder(mContext) - .androidIdDisabled(false), - ) - Assert.assertTrue(MParticle.isAndroidIdEnabled()) - Assert.assertFalse(MParticle.isAndroidIdDisabled()) - // test androidIdEnabled == true startMParticle( MParticleOptions @@ -203,7 +180,6 @@ class MParticleOptionsTest : BaseAbstractTest() { .androidIdEnabled(true), ) Assert.assertTrue(MParticle.isAndroidIdEnabled()) - Assert.assertFalse(MParticle.isAndroidIdDisabled()) } @Test @@ -754,14 +730,14 @@ class MParticleOptionsTest : BaseAbstractTest() { MParticleOptions .builder(mContext) .credentials("this", "that") - .androidIdDisabled(true) + .androidIdEnabled(false) .build() Assert.assertTrue(infoLogs.contains("ANDROID_ID will not be collected based on MParticleOptions settings")) infoLogs.clear() MParticleOptions .builder(mContext) .credentials("this", "that") - .androidIdDisabled(false) + .androidIdEnabled(true) .build() Assert.assertTrue(infoLogs.contains("ANDROID_ID will be collected based on MParticleOptions settings")) infoLogs.clear() diff --git a/android-core/src/androidTest/kotlin/com.mparticle/identity/MParticleUserDelegateITest.kt b/android-core/src/androidTest/kotlin/com.mparticle/identity/MParticleUserDelegateITest.kt index 890b288b3..1a709d096 100644 --- a/android-core/src/androidTest/kotlin/com.mparticle/identity/MParticleUserDelegateITest.kt +++ b/android-core/src/androidTest/kotlin/com.mparticle/identity/MParticleUserDelegateITest.kt @@ -5,7 +5,7 @@ import android.os.Looper import android.util.Log import com.mparticle.MParticle import com.mparticle.MParticle.IdentityType -import com.mparticle.UserAttributeListener +import com.mparticle.TypedUserAttributeListener import com.mparticle.consent.CCPAConsent import com.mparticle.consent.ConsentState import com.mparticle.consent.GDPRConsent @@ -191,14 +191,20 @@ class MParticleUserDelegateITest : BaseCleanStartedEachTest() { mUserDelegate.setUserAttribute(key, value, mStartingMpid) } AccessUtils.awaitMessageHandler() - val userAttributesResults = AndroidUtils.Mutable?>(null) - val userAttributeListResults = AndroidUtils.Mutable>?>(null) + val userAttributesResults = AndroidUtils.Mutable?>(null) + val userAttributeListResults = AndroidUtils.Mutable?>?>(null) // fetch on the current (non-main) thread mUserDelegate.getUserAttributes( - UserAttributeListener { userAttributes, userAttributeLists, mpid -> - userAttributesResults.value = userAttributes - userAttributeListResults.value = userAttributeLists + object : TypedUserAttributeListener { + override fun onUserAttributesReceived( + userAttributes: Map, + userAttributeLists: Map?>, + mpid: Long, + ) { + userAttributesResults.value = userAttributes + userAttributeListResults.value = userAttributeLists + } }, mStartingMpid, ) @@ -211,10 +217,16 @@ class MParticleUserDelegateITest : BaseCleanStartedEachTest() { val latch: CountDownLatch = MPLatch(1) Handler(Looper.getMainLooper()).post { mUserDelegate.getUserAttributes( - UserAttributeListener { userAttributes, userAttributeLists, mpid -> - userAttributesResults.value = userAttributes - userAttributeListResults.value = userAttributeLists - latch.countDown() + object : TypedUserAttributeListener { + override fun onUserAttributesReceived( + userAttributes: Map, + userAttributeLists: Map?>, + mpid: Long, + ) { + userAttributesResults.value = userAttributes + userAttributeListResults.value = userAttributeLists + latch.countDown() + } }, mStartingMpid, ) diff --git a/android-core/src/androidTest/kotlin/com.mparticle/startup/StartupTest.kt b/android-core/src/androidTest/kotlin/com.mparticle/startup/StartupTest.kt index 3c47f9e51..f1f00d782 100644 --- a/android-core/src/androidTest/kotlin/com.mparticle/startup/StartupTest.kt +++ b/android-core/src/androidTest/kotlin/com.mparticle/startup/StartupTest.kt @@ -119,7 +119,7 @@ class StartupTest : BaseStartupTest() { .userIdentities(identityMap) .build(), ).logLevel(MParticle.LogLevel.DEBUG) - .androidIdDisabled(false) + .androidIdEnabled(true) .attributionListener( object : AttributionListener { override fun onResult(result: AttributionResult) { diff --git a/android-core/src/main/java/com/mparticle/MPEvent.java b/android-core/src/main/java/com/mparticle/MPEvent.java index 651c2b3bc..a222b56fe 100644 --- a/android-core/src/main/java/com/mparticle/MPEvent.java +++ b/android-core/src/main/java/com/mparticle/MPEvent.java @@ -96,15 +96,6 @@ public boolean equals(@Nullable Object o) { return super.equals(o) || (o != null && this.toString().equals(o.toString())); } - /** - * @param info - * @deprecated use {@link MPEvent#setCustomAttributes(Map)} instead - */ - @Deprecated - public void setInfo(@Nullable Map info) { - setCustomAttributes(info); - } - @Override public void setCustomAttributes(@NonNull Map customAttributes) { super.setCustomAttributes(customAttributes); @@ -196,16 +187,6 @@ public String getCategory() { return category; } - /** - * @return - * @deprecated use {@link MPEvent#getCustomAttributes()} instead - */ - @Deprecated - @Nullable - public Map getInfo() { - return getCustomAttributeStrings(); - } - @NonNull public MParticle.EventType getEventType() { return eventType; @@ -389,20 +370,6 @@ public Builder duration(double durationMillis) { return this; } - /** - * @param info - * @return returns this builder for easy method chaining - * @deprecated user {@link MPEvent.Builder#customAttributes} instead - * - * Data attributes to associate with the event. - */ - @Deprecated - @NonNull - public Builder info(@Nullable Map info) { - this.customAttributes = info; - return this; - } - @NonNull public Builder customAttributes(@Nullable Map customAttributes) { this.customAttributes = customAttributes; diff --git a/android-core/src/main/java/com/mparticle/MParticle.java b/android-core/src/main/java/com/mparticle/MParticle.java index a8c2f955f..76cf029ed 100644 --- a/android-core/src/main/java/com/mparticle/MParticle.java +++ b/android-core/src/main/java/com/mparticle/MParticle.java @@ -111,7 +111,6 @@ public class MParticle { protected boolean locationTrackingEnabled = false; @NonNull protected Internal mInternal = new Internal(); - protected Rokt rokt; private IdentityStateListener mDeferredModifyPushRegistrationListener; @NonNull private WrapperSdkVersion wrapperSdkVersion = new WrapperSdkVersion(WrapperSdk.WrapperNone, null); @@ -191,7 +190,6 @@ private static MParticle getInstance(@NonNull Context context, @NonNull MParticl instance = new MParticle(options); instance.mKitManager = new KitFrameworkWrapper(options.getContext(), instance.mMessageManager, instance.Internal().getConfigManager(), instance.Internal().getAppStateManager(), options); instance.mIdentityApi = new IdentityApi(options.getContext(), instance.mInternal.getAppStateManager(), instance.mMessageManager, instance.mConfigManager, instance.mKitManager, options.getOperatingSystem()); - instance.rokt = new Rokt(instance.mConfigManager, instance.mKitManager); // Check if we've switched workspaces on startup UploadSettings lastUploadSettings = instance.mConfigManager.getLastUploadSettings(); @@ -336,23 +334,6 @@ private static void performWorkspaceSwitch(@NonNull MParticleOptions options) { }); } - /** - * @return false if Android ID collection is enabled. (true by default) - * @see MParticleOptions.Builder#androidIdEnabled(boolean) - * @deprecated This method has been replaced as the behavior has been inverted - Android ID collection is now disabled by default. - *

Use {@link MParticle#isAndroidIdEnabled()} instead. - *

- *

- * Query the status of Android ID collection. - *

- * By default, the SDK will NOT collect Android Id for the purpose - * of anonymous analytics. If you're not using an mParticle integration that consumes Android ID and you would like to collect it, use this API to enable collection - */ - @Deprecated - public static boolean isAndroidIdDisabled() { - return !sAndroidIdEnabled; - } - /** * Query the status of Android ID collection. *

@@ -991,16 +972,6 @@ public void disableUncaughtExceptionLogging() { mConfigManager.disableUncaughtExceptionLogging(true); } - /** - * @return The current setting of automatic screen tracking. - * @deprecated Retrieves the current setting of automatic screen tracking. - */ - @NonNull - @Deprecated - public Boolean isAutoTrackingEnabled() { - return false; - } - /** * Retrieves the current session timeout setting in seconds * @@ -1111,28 +1082,6 @@ public void onAudioStopped() { return mMedia; } - /** - * Detect whether the given service provider is active. Use this method - * only when you need to make direct calls to an embedded SDK. - *

- * You can also register a {@link android.content.BroadcastReceiver} with an {@link android.content.IntentFilter}, using an action of - * {@link MParticle.ServiceProviders#BROADCAST_ACTIVE} or {@link MParticle.ServiceProviders#BROADCAST_DISABLED} - * concatenated with the service provider ID: - * - *

-     * {@code
-     * Context.registerReceiver(yourReceiver, new IntentFilter(MParticle.ServiceProviders.BROADCAST_ACTIVE + MParticle.ServiceProviders.APPBOY));}
-     * 
- * - * @param serviceProviderId - * @return True if you can safely make direct calls to the given service provider. - * @see MParticle.ServiceProviders - * @deprecated - */ - public boolean isProviderActive(int serviceProviderId) { - return isKitActive(serviceProviderId); - } - /** * Detect whether the given service provider kit is active. Use this method * only when you need to make direct calls to an embedded SDK. @@ -1220,11 +1169,6 @@ public void logNotificationOpened(@NonNull Intent intent) { public Internal Internal() { return mInternal; } - @NonNull - public Rokt Rokt() { - return rokt; - } - void refreshConfiguration() { Logger.debug("Refreshing configuration..."); mMessageManager.refreshConfiguration(); diff --git a/android-core/src/main/java/com/mparticle/MParticleOptions.java b/android-core/src/main/java/com/mparticle/MParticleOptions.java index 2e068d8d2..c5e421e8b 100644 --- a/android-core/src/main/java/com/mparticle/MParticleOptions.java +++ b/android-core/src/main/java/com/mparticle/MParticleOptions.java @@ -272,19 +272,6 @@ public Boolean isDeviceBasedConsentEnabled() { return mDeviceBasedConsentEnabled; } - /** - * @return true if collection is disabled, false if it is enabled - * @deprecated This method has been replaced as the behavior has been inverted - Android ID collection is now disabled by default. - *

Use {@link MParticle#isAndroidIdEnabled()} instead. - *

- * Query whether Android Id collection is enabled or disabled. - */ - @NonNull - @Deprecated - public Boolean isAndroidIdDisabled() { - return !mAndroidIdEnabled; - } - /** * Query whether Android Id collection is enabled or disabled. * @@ -606,23 +593,6 @@ public Builder deviceBasedConsentEnabled(boolean enabled) { return this; } - /** - * @param disabled false to enable collection (true by default) - * @return the instance of the builder, for chaining calls - * @deprecated This method has been replaced as the behavior has been inverted - Android ID collection is now disabled by default. - *

Use {@link androidIdEnabled(boolean)} instead. - *

- *

- * By default, the SDK will NOT collect Android Id for the purpose - * of anonymous analytics. If you're not using an mParticle integration that consumes Android ID and you would like to collect it, use this API to enable collection. - */ - @NonNull - @Deprecated - public Builder androidIdDisabled(boolean disabled) { - this.androidIdEnabled = !disabled; - return this; - } - /** * By default, the SDK will NOT collect Android Id for the purpose * of anonymous analytics. If you're not using an mParticle integration that consumes Android ID and you would like to collect it, use this API to enable collection diff --git a/android-core/src/main/java/com/mparticle/UserAttributeListener.java b/android-core/src/main/java/com/mparticle/UserAttributeListener.java deleted file mode 100644 index 2b79fecca..000000000 --- a/android-core/src/main/java/com/mparticle/UserAttributeListener.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.mparticle; - -import androidx.annotation.Nullable; - -import java.util.List; -import java.util.Map; - -/** - * @deprecated Use TypedUserAttributeListener instead - */ -@Deprecated -public interface UserAttributeListener extends UserAttributeListenerType { - void onUserAttributesReceived(@Nullable Map userAttributes, @Nullable Map> userAttributeLists, @Nullable Long mpid); -} diff --git a/android-core/src/main/java/com/mparticle/consent/ConsentState.java b/android-core/src/main/java/com/mparticle/consent/ConsentState.java index 7297266c4..499e0ef31 100644 --- a/android-core/src/main/java/com/mparticle/consent/ConsentState.java +++ b/android-core/src/main/java/com/mparticle/consent/ConsentState.java @@ -201,24 +201,12 @@ public Builder removeGDPRConsentState(@NonNull String purpose) { return this; } - @Deprecated - @NonNull - public Builder setCCPAConsent(@NonNull CCPAConsent ccpaConsent) { - return setCCPAConsentState(ccpaConsent); - } - @NonNull public Builder setCCPAConsentState(@NonNull CCPAConsent ccpaConsent) { this.ccpaConsent = ccpaConsent; return this; } - @Deprecated - @NonNull - public Builder removeCCPAConsent() { - return removeCCPAConsentState(); - } - @NonNull public Builder removeCCPAConsentState() { ccpaConsent = null; diff --git a/android-core/src/main/java/com/mparticle/identity/IdentityApi.java b/android-core/src/main/java/com/mparticle/identity/IdentityApi.java index c1a5246ad..0f0253e9a 100644 --- a/android-core/src/main/java/com/mparticle/identity/IdentityApi.java +++ b/android-core/src/main/java/com/mparticle/identity/IdentityApi.java @@ -373,7 +373,7 @@ public void run() { long newMpid = result.getMpId(); boolean isLoggedIn = result.isLoggedIn(); ConfigManager.setIdentityRequestInProgress(false); - mUserDelegate.setUser(mContext, startingMpid, newMpid, identityApiRequest.getUserIdentities(), identityApiRequest.getUserAliasHandler(), isLoggedIn); + mUserDelegate.setUser(mContext, startingMpid, newMpid, identityApiRequest.getUserIdentities(), isLoggedIn); final MParticleUser previousUser = startingMpid != newMpid ? getUser(startingMpid) : null; task.setSuccessful(new IdentityApiResult(MParticleUserImpl.getInstance(mContext, newMpid, mUserDelegate), previousUser)); new Handler(Looper.getMainLooper()).post(new Runnable() { diff --git a/android-core/src/main/java/com/mparticle/identity/IdentityApiRequest.java b/android-core/src/main/java/com/mparticle/identity/IdentityApiRequest.java index 1a8c42cdc..f7e071a3d 100644 --- a/android-core/src/main/java/com/mparticle/identity/IdentityApiRequest.java +++ b/android-core/src/main/java/com/mparticle/identity/IdentityApiRequest.java @@ -22,7 +22,6 @@ * @see IdentityApi#modify(IdentityApiRequest) */ public final class IdentityApiRequest { - private UserAliasHandler userAliasHandler = null; private Map userIdentities = new HashMap(); // for /modify requests private Map otherOldIdentities = new HashMap(); @@ -33,9 +32,6 @@ private IdentityApiRequest(IdentityApiRequest.Builder builder) { if (builder.userIdentities != null) { this.userIdentities = builder.userIdentities; } - if (builder.userAliasHandler != null) { - this.userAliasHandler = builder.userAliasHandler; - } if (builder.otherOldIdentities.size() == builder.otherNewIdentities.size()) { this.otherNewIdentities = builder.otherNewIdentities; this.otherOldIdentities = builder.otherOldIdentities; @@ -83,11 +79,6 @@ protected Map getOtherNewIdentities() { return otherNewIdentities; } - @Nullable - public UserAliasHandler getUserAliasHandler() { - return userAliasHandler; - } - /** * A class used for constructing IdentityApiRequest. */ @@ -96,7 +87,6 @@ public static class Builder { private Map userIdentities = new HashMap(); private Map otherOldIdentities = new HashMap(); private Map otherNewIdentities = new HashMap(); - private UserAliasHandler userAliasHandler; protected Builder(@Nullable MParticleUser currentUser) { if (currentUser != null) { @@ -192,23 +182,5 @@ public Builder userIdentities(@NonNull Map userI public IdentityApiRequest build() { return new IdentityApiRequest(this); } - - /** - * Deprecated. To easily migrate your existing code, add a success listener - * to the {@link BaseIdentityTask} that is being returned from this method ){@link BaseIdentityTask#addSuccessListener(TaskSuccessListener)}. Within the - * {@link IdentityApiResult} returned by the success listener, you can run the same code you do - * in you {@link UserAliasHandler}, using the {@link MParticleUser}s returned by - * {@link IdentityApiResult#getUser()} and {@link IdentityApiResult#getPreviousUser()} in place - * of "newUser" and "previousUser" respectively - * - * @param userAliasHandler - * @return - */ - @Deprecated - @NonNull - public Builder userAliasHandler(@Nullable UserAliasHandler userAliasHandler) { - this.userAliasHandler = userAliasHandler; - return this; - } } } \ No newline at end of file diff --git a/android-core/src/main/java/com/mparticle/identity/MParticleUserDelegate.java b/android-core/src/main/java/com/mparticle/identity/MParticleUserDelegate.java index f3d98a1e9..1c20186a5 100644 --- a/android-core/src/main/java/com/mparticle/identity/MParticleUserDelegate.java +++ b/android-core/src/main/java/com/mparticle/identity/MParticleUserDelegate.java @@ -249,7 +249,7 @@ static void setUserIdentities(MParticleUserDelegate userDelegate, Map identities, UserAliasHandler userAliasHandler, boolean isLoggedIn) { + boolean setUser(Context context, long previousMpid, long newMpid, Map identities, boolean isLoggedIn) { setUserIdentities(this, identities, newMpid); // if the mpid remains equal to the temporary_mpid, as the case could be when a network request fails // or on startup, then there is no reason to do anything @@ -259,17 +259,6 @@ boolean setUser(Context context, long previousMpid, long newMpid, Map, - callbacks: MpRoktEventCallback? = null, - embeddedViews: Map>? = null, - fontTypefaces: Map>? = null, - config: RoktConfig? = null, - ) { - if (mConfigManager.isEnabled) { - val roktApi = mKitManager.roktKitApi - if (roktApi != null) { - roktApi.selectPlacements(identifier, HashMap(attributes), callbacks, embeddedViews, fontTypefaces, config, buildPlacementOptions()) - } else { - Logger.warning("Rokt Kit is not available. Make sure the Rokt Kit is included in your app.") - } - } - } - - /** - * Get a Flow of Rokt events for the specified identifier. - * - * @param identifier The placement identifier to listen for events - * @return A Flow emitting RoktEvent objects - */ - fun events(identifier: String): Flow = if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.events(identifier) ?: flowOf() - } else { - flowOf() - } - - /** - * Notify Rokt that a purchase has been finalized. - * - * @param placementId The placement identifier - * @param catalogItemId The catalog item identifier - * @param status Whether the purchase was successful - */ - fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { - if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.purchaseFinalized(placementId, catalogItemId, status) - } - } - - /** - * Close any active Rokt placements. - */ - fun close() { - if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.close() - } - } - - /** - * Set the session id to use for the next execute call. - * - * This is useful for cases where you have a session id from a non-native integration, - * e.g. WebView, and you want the session to be consistent across integrations. - * - * **Note:** Empty strings are ignored and will not update the session. - * - * @param sessionId The session id to be set. Must be a non-empty string. - */ - fun setSessionId(sessionId: String) { - if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.setSessionId(sessionId) - } - } - - /** - * Get the session id to use within a non-native integration e.g. WebView. - * - * @return The session id or null if no session is present or SDK is not initialized. - */ - fun getSessionId(): String? = if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.getSessionId() - } else { - null - } - - /** - * Prepare attributes asynchronously before executing a placement. - * - * @param attributes The attributes to prepare - */ - fun prepareAttributesAsync(attributes: Map) { - if (mConfigManager.isEnabled) { - mKitManager.roktKitApi?.prepareAttributesAsync(attributes) - } - } - - private fun buildPlacementOptions(): PlacementOptions = PlacementOptions( - jointSdkSelectPlacements = System.currentTimeMillis(), - ) -} diff --git a/android-core/src/main/kotlin/com/mparticle/RoktEvent.kt b/android-core/src/main/kotlin/com/mparticle/RoktEvent.kt deleted file mode 100644 index fa9d6aa85..000000000 --- a/android-core/src/main/kotlin/com/mparticle/RoktEvent.kt +++ /dev/null @@ -1,102 +0,0 @@ -package com.mparticle - -// RoktEvent interface for handling events from the Rokt SDK. -sealed interface RoktEvent { - /** - * ShowLoadingIndicator event will be triggered before SDK calls Rokt backend - */ - data object ShowLoadingIndicator : RoktEvent - - /** - * HideLoadingIndicator event will be triggered when SDK obtains a success or failure from - * Rokt backend - */ - data object HideLoadingIndicator : RoktEvent - - /** - * OfferEngagement event will be triggered if User engaged with the offer - * @param placementId - identifier for the placement emitting the event - */ - data class OfferEngagement(val placementId: String) : RoktEvent - - /** - * PositiveEngagement event will be triggered if User positively engaged with the offer - * @param placementId - identifier for the placement emitting the event - */ - data class PositiveEngagement(val placementId: String) : RoktEvent - - /** - * FirstPositiveEngagement event will be triggered when the user positively engaged with the offer first time - * @param placementId - identifier for the placement emitting the event - */ - data class FirstPositiveEngagement(val placementId: String) : RoktEvent - - /** - * PlacementInteractive event will be triggered when placement has been rendered and is interactable - * @param placementId - identifier for the placement emitting the event - */ - data class PlacementInteractive(val placementId: String) : RoktEvent - - /** - * PlacementReady event will be triggered when placement is ready to display but has not rendered content yet - * @param placementId - identifier for the placement emitting the event - */ - data class PlacementReady(val placementId: String) : RoktEvent - - /** - * PlacementClosed event will be triggered when placement closes by user - * @param placementId - identifier for the placement emitting the event - */ - data class PlacementClosed(val placementId: String) : RoktEvent - - /** - * PlacementCompleted event will be triggered when the offer progression moves to the end and no more - * offer to display - * @param placementId - identifier for the placement emitting the event - */ - data class PlacementCompleted(val placementId: String) : RoktEvent - - /** - * PlacementFailure event will be triggered when placement could not be displayed due to some failure - * @param placementId - optional identifier for the placement emitting the event - */ - data class PlacementFailure(val placementId: String? = null) : RoktEvent - - /** - * InitComplete event will be triggered when SDK has finished initialization - * @param success - true if init was successful - */ - data class InitComplete(val success: Boolean) : RoktEvent - - /** - * OpenUrl event will be triggered when user clicks on a link and the link target is set to Passthrough - * @param placementId - identifier for the placement emitting the event - * @param url - url to open - */ - data class OpenUrl(val placementId: String, val url: String) : RoktEvent - - /** - * CartItemInstantPurchase event will be triggered when the catalog item purchase is initiated - * by the user - * @property placementId The layout identifier. - * @property cartItemId The cart item identifier. - * @property catalogItemId The catalog item identifier. - * @property currency The currency used for the purchase. - * @property description The description of the cart item. - * @property linkedProductId The linked product identifier. - * @property totalPrice The total price of the cart item. - * @property quantity The quantity of the cart item. - * @property unitPrice The unit price of the cart item. - */ - data class CartItemInstantPurchase( - val placementId: String, - val cartItemId: String, - val catalogItemId: String, - val currency: String, - val description: String, - val linkedProductId: String, - val totalPrice: Double, - val quantity: Int, - val unitPrice: Double, - ) : RoktEvent -} diff --git a/android-core/src/main/kotlin/com/mparticle/identity/UserAttributeListenerWrapper.kt b/android-core/src/main/kotlin/com/mparticle/identity/UserAttributeListenerWrapper.kt index 47836bdd4..a87b41d45 100644 --- a/android-core/src/main/kotlin/com/mparticle/identity/UserAttributeListenerWrapper.kt +++ b/android-core/src/main/kotlin/com/mparticle/identity/UserAttributeListenerWrapper.kt @@ -1,17 +1,11 @@ package com.mparticle.identity import com.mparticle.TypedUserAttributeListener -import com.mparticle.UserAttributeListener import com.mparticle.UserAttributeListenerType class UserAttributeListenerWrapper(val listener: UserAttributeListenerType) { fun onUserAttributesReceived(singles: Map?, lists: Map?>?, mpid: Long?) { when (listener) { - is UserAttributeListener -> - (singles ?: mutableMapOf()) - .entries - .associate { it.key to it.value?.toString() } - .let { listener.onUserAttributesReceived(it, lists, mpid) } is TypedUserAttributeListener -> mpid?.let { listener.onUserAttributesReceived( diff --git a/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt b/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt index 0387841f4..5df1e47e7 100644 --- a/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt +++ b/android-core/src/main/kotlin/com/mparticle/internal/AppStateManager.kt @@ -11,7 +11,6 @@ import android.os.Bundle import android.os.Handler import android.os.Looper import android.os.SystemClock -import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.identity.IdentityApi.SingleUserIdentificationCallback import com.mparticle.identity.IdentityApiRequest @@ -182,11 +181,6 @@ constructor(context: Context, unitTesting: Boolean = false) { val instance = MParticle.getInstance() if (instance != null) { - if (instance.isAutoTrackingEnabled) { - currentActivityName?.let { - instance.logScreen(it) - } - } if (isBackToForeground) { instance.Internal().kitManager.onApplicationForeground() Logger.debug("App foregrounded.") @@ -223,14 +217,6 @@ constructor(context: Context, unitTesting: Boolean = false) { val instance = MParticle.getInstance() if (instance != null) { - if (instance.isAutoTrackingEnabled) { - instance.logScreen( - MPEvent - .Builder(getActivityName(activity)) - .internalNavigationDirection(false) - .build(), - ) - } instance.Internal().kitManager.onActivityPaused(activity) } } catch (e: Exception) { diff --git a/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt b/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt deleted file mode 100644 index 68f9c628d..000000000 --- a/android-core/src/main/kotlin/com/mparticle/internal/RoktKitApi.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.mparticle.internal - -import android.graphics.Typeface -import com.mparticle.MpRoktEventCallback -import com.mparticle.RoktEvent -import com.mparticle.rokt.PlacementOptions -import com.mparticle.rokt.RoktConfig -import com.mparticle.rokt.RoktEmbeddedView -import kotlinx.coroutines.flow.Flow -import java.lang.ref.WeakReference - -/** - * Interface for Rokt Kit operations. - * - * Implementations of this interface are provided by the Rokt Kit when it is - * configured and active. Use [KitManager.getRoktKitApi] to obtain an instance. - */ -interface RoktKitApi { - /** - * Initiate a Rokt placement selection with the specified parameters. - * - * @param viewName The identifier for the placement view - * @param attributes User attributes to pass to Rokt - * @param mpRoktEventCallback Optional callback for Rokt events - * @param placeHolders Optional map of embedded view placeholders - * @param fontTypefaces Optional map of font typefaces - * @param config Optional Rokt configuration - * @param options Optional placement options - */ - fun selectPlacements( - viewName: String, - attributes: Map, - mpRoktEventCallback: MpRoktEventCallback?, - placeHolders: Map>?, - fontTypefaces: Map>?, - config: RoktConfig?, - options: PlacementOptions? = null, - ) - - /** - * Get a Flow of Rokt events for the specified identifier. - * - * @param identifier The placement identifier to listen for events - * @return A Flow emitting RoktEvent objects - */ - fun events(identifier: String): Flow - - /** - * Notify Rokt that a purchase has been finalized. - * - * @param placementId The placement identifier - * @param catalogItemId The catalog item identifier - * @param status Whether the purchase was successful - */ - fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) - - /** - * Close any active Rokt placements. - */ - fun close() - - /** - * Set the session id to use for the next execute call. - * - * This is useful for cases where you have a session id from a non-native integration, - * e.g. WebView, and you want the session to be consistent across integrations. - * - * **Note:** Empty strings are ignored and will not update the session. - * - * @param sessionId The session id to be set. Must be a non-empty string. - */ - fun setSessionId(sessionId: String) - - /** - * Get the session id to use within a non-native integration e.g. WebView. - * - * @return The session id or null if no session is present. - */ - fun getSessionId(): String? - - /** - * Prepare attributes asynchronously before executing a placement. - * - * @param attributes The attributes to prepare - */ - fun prepareAttributesAsync(attributes: Map) -} diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt b/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt deleted file mode 100644 index c3f2a3417..000000000 --- a/android-core/src/main/kotlin/com/mparticle/rokt/PlacementOptions.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.mparticle.rokt - -data class PlacementOptions(val jointSdkSelectPlacements: Long, val dynamicPerformanceMarkers: Map = mapOf()) diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/RoktConfig.kt b/android-core/src/main/kotlin/com/mparticle/rokt/RoktConfig.kt deleted file mode 100644 index 28488a0e1..000000000 --- a/android-core/src/main/kotlin/com/mparticle/rokt/RoktConfig.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.mparticle.rokt - -class RoktConfig private constructor(val colorMode: ColorMode?, val cacheConfig: CacheConfig?, val edgeToEdgeDisplay: Boolean) { - data class Builder( - private var colorMode: ColorMode? = null, - private var cacheConfig: CacheConfig? = null, - private var edgeToEdgeDisplay: Boolean = true, - ) { - fun colorMode(mode: ColorMode) = apply { this.colorMode = mode } - - fun cacheConfig(cacheConfig: CacheConfig) = apply { this.cacheConfig = cacheConfig } - - fun edgeToEdgeDisplay(edgeToEdgeDisplay: Boolean) = apply { this.edgeToEdgeDisplay = edgeToEdgeDisplay } - - fun build(): RoktConfig = RoktConfig(colorMode, cacheConfig, edgeToEdgeDisplay) - } - - enum class ColorMode { LIGHT, DARK, SYSTEM } -} - -class CacheConfig(val cacheDurationInSeconds: Long = DEFAULT_CACHE_DURATION_SECS, val cacheAttributes: Map? = null) { - companion object { - const val DEFAULT_CACHE_DURATION_SECS: Long = 90 * 60 - } -} diff --git a/android-core/src/main/kotlin/com/mparticle/rokt/RoktLayoutDimensionCallBack.kt b/android-core/src/main/kotlin/com/mparticle/rokt/RoktLayoutDimensionCallBack.kt deleted file mode 100644 index e22535c90..000000000 --- a/android-core/src/main/kotlin/com/mparticle/rokt/RoktLayoutDimensionCallBack.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.mparticle.rokt - -interface RoktLayoutDimensionCallBack { - fun onHeightChanged(height: Int) - - fun onMarginChanged(start: Int, top: Int, end: Int, bottom: Int) -} diff --git a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt b/android-core/src/test/kotlin/com/mparticle/RoktTest.kt deleted file mode 100644 index 317f58626..000000000 --- a/android-core/src/test/kotlin/com/mparticle/RoktTest.kt +++ /dev/null @@ -1,287 +0,0 @@ -package com.mparticle - -import android.graphics.Typeface -import android.os.Looper -import android.os.SystemClock -import com.mparticle.internal.ConfigManager -import com.mparticle.internal.KitManager -import com.mparticle.internal.RoktKitApi -import com.mparticle.rokt.PlacementOptions -import com.mparticle.rokt.RoktConfig -import com.mparticle.rokt.RoktEmbeddedView -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers -import org.mockito.ArgumentMatchers.isNull -import org.mockito.Mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.MockitoAnnotations -import org.powermock.core.classloader.annotations.PrepareForTest -import org.powermock.modules.junit4.PowerMockRunner -import java.lang.ref.WeakReference -import kotlin.test.assertEquals -import kotlin.test.assertNull -import kotlin.test.assertTrue - -@RunWith(PowerMockRunner::class) -@PrepareForTest(Looper::class, SystemClock::class) -class RoktTest { - @Mock - lateinit var kitManager: KitManager - - @Mock - lateinit var roktKitApi: RoktKitApi - - @Mock - lateinit var configManager: ConfigManager - private lateinit var rokt: Rokt - - // Helpers to make Mockito matchers work in Kotlin with non-nullable types. - // Mockito matchers return null, which Kotlin rejects for non-nullable params. - // These helpers call the matcher (to register it) then return a cast null. - private fun capture(captor: ArgumentCaptor): T { - captor.capture() - @Suppress("UNCHECKED_CAST") - return null as T - } - - private fun any(): T { - ArgumentMatchers.any() - @Suppress("UNCHECKED_CAST") - return null as T - } - - private fun eq(value: T): T { - ArgumentMatchers.eq(value) - @Suppress("UNCHECKED_CAST") - return null as T - } - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - rokt = Rokt(configManager, kitManager) - } - - @Test - fun testSelectPlacements_withFullParams_whenEnabled() { - `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - val attributes = mutableMapOf() - attributes["key"] = "value" - - val placeholders: Map> = HashMap() - val fonts: Map> = HashMap() - - val config = RoktConfig.Builder().colorMode(RoktConfig.ColorMode.DARK).build() - - val callbacks = - object : MpRoktEventCallback { - override fun onLoad() { - println("View loaded") - } - - override fun onUnload(reason: UnloadReasons) { - println("View unloaded due to: $reason") - } - - override fun onShouldShowLoadingIndicator() { - println("Show loading indicator") - } - - override fun onShouldHideLoadingIndicator() { - println("Hide loading indicator") - } - } - rokt.selectPlacements( - identifier = "testView", - attributes = attributes, - callbacks = callbacks, - embeddedViews = placeholders, - fontTypefaces = fonts, - config = config, - ) - - verify(roktKitApi).selectPlacements( - eq("testView"), - eq(attributes), - any(), - any(), - any(), - any(), - any(), - ) - } - - @Test - fun testSelectPlacements_withBasicParams_whenEnabled() { - `when`(configManager.isEnabled()).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - val attributes = mutableMapOf() - attributes["a"] = "b" - - rokt.selectPlacements(attributes = attributes, identifier = "basicView") - - verify(roktKitApi).selectPlacements( - eq("basicView"), - eq(attributes), - isNull(), - isNull(), - isNull(), - isNull(), - any(), - ) - } - - @Test - fun testSelectPlacements_withBasicParams_whenDisabled() { - `when`(configManager.isEnabled()).thenReturn(false) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - rokt.selectPlacements( - identifier = "basicView", - attributes = HashMap(), - ) - - verify(roktKitApi, never()).selectPlacements(any(), any(), any(), any(), any(), any(), any()) - } - - @Test - fun testRoktSetWrapperSdk_whenDisabled_kitManagerNotCalled() { - `when`(configManager.isEnabled()).thenReturn(false) - - rokt.selectPlacements( - identifier = "basicView", - attributes = HashMap(), - ) - - verify(kitManager, never()).setWrapperSdkVersion(any()) - } - - @Test - fun testReportConversion_withBasicParams_whenEnabled() { - `when`(configManager.isEnabled()).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - val attributes = mutableMapOf() - attributes["a"] = "b" - - rokt.purchaseFinalized("132", "1111", true) - - verify(roktKitApi).purchaseFinalized("132", "1111", true) - } - - @Test - fun testReportConversion_withBasicParams_whenDisabled() { - `when`(configManager.isEnabled()).thenReturn(false) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - rokt.purchaseFinalized("132", "1111", true) - - verify(roktKitApi, never()).purchaseFinalized("132", "1111", true) - } - - @Test - fun testEvents_whenEnabled_delegatesToKitManager() { - `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - val testIdentifier = "test-identifier" - val expectedFlow: Flow = flowOf() - `when`(roktKitApi.events(testIdentifier)).thenReturn(expectedFlow) - - val result = rokt.events(testIdentifier) - - verify(roktKitApi).events(testIdentifier) - assertEquals(expectedFlow, result) - } - - @Test - fun testEvents_whenDisabled_returnsEmptyFlow() { - `when`(configManager.isEnabled).thenReturn(false) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - - val testIdentifier = "test-identifier" - - val result = rokt.events(testIdentifier) - - verify(roktKitApi, never()).events(any()) - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - @Test - fun testSetSessionId_whenEnabled_delegatesToKitManager() { - `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - rokt.setSessionId("test-session-id") - verify(roktKitApi).setSessionId("test-session-id") - } - - @Test - fun testSetSessionId_whenDisabled_doesNotCallKitManager() { - `when`(configManager.isEnabled).thenReturn(false) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - rokt.setSessionId("test-session-id") - verify(roktKitApi, never()).setSessionId(any()) - } - - @Test - fun testGetSessionId_whenEnabled_delegatesToKitManager() { - `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - `when`(roktKitApi.getSessionId()).thenReturn("expected-session-id") - val result = rokt.getSessionId() - verify(roktKitApi).getSessionId() - assertEquals("expected-session-id", result) - } - - @Test - fun testGetSessionId_whenDisabled_returnsNull() { - `when`(configManager.isEnabled).thenReturn(false) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - val result = rokt.getSessionId() - verify(roktKitApi, never()).getSessionId() - assertNull(result) - } - - @Test - fun testSelectPlacements_withOptions_whenEnabled() { - `when`(configManager.isEnabled).thenReturn(true) - `when`(kitManager.roktKitApi).thenReturn(roktKitApi) - val currentTimeMillis = System.currentTimeMillis() - - val attributes = mutableMapOf() - - rokt.selectPlacements( - identifier = "testView", - attributes = attributes, - ) - - // Verify call is forwarded - val viewNameCaptor = ArgumentCaptor.forClass(String::class.java) - val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) - verify(roktKitApi).selectPlacements( - eq("testView"), - any(), - isNull(), - isNull(), - isNull(), - isNull(), - capture(optionsCaptor), - ) - assertTrue(optionsCaptor.value.jointSdkSelectPlacements >= currentTimeMillis) - } -} diff --git a/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt b/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt index 22a9db09b..3017d6bbf 100644 --- a/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/external/ApiVisibilityTest.kt @@ -17,7 +17,7 @@ class ApiVisibilityTest { publicMethodCount++ } } - Assert.assertEquals(69, publicMethodCount) + Assert.assertEquals(65, publicMethodCount) } @Test diff --git a/android-core/src/test/kotlin/com/mparticle/identity/MParticleUserTest.kt b/android-core/src/test/kotlin/com/mparticle/identity/MParticleUserTest.kt index b1ca27fd7..89427cb4b 100644 --- a/android-core/src/test/kotlin/com/mparticle/identity/MParticleUserTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/identity/MParticleUserTest.kt @@ -4,7 +4,6 @@ import com.mparticle.MParticle import com.mparticle.MParticle.IdentityType import com.mparticle.MockMParticle import com.mparticle.TypedUserAttributeListener -import com.mparticle.UserAttributeListener import org.json.JSONArray import org.json.JSONObject import org.junit.Assert @@ -153,7 +152,7 @@ class MParticleUserTest { fun testGetAllUserAttributes() { val listener = Mockito.mock( - UserAttributeListener::class.java, + TypedUserAttributeListener::class.java, ) id?.currentUser?.getUserAttributes(listener) Mockito @@ -166,21 +165,6 @@ class MParticleUserTest { ), ArgumentMatchers.eq(defaultMpId), ) - val typedListener = - Mockito.mock( - TypedUserAttributeListener::class.java, - ) - id?.currentUser?.getUserAttributes(typedListener) - Mockito - .verify( - mp?.Identity()?.mMessageManager, - Mockito.times(2), - )?.getUserAttributes( - ArgumentMatchers.any( - UserAttributeListenerWrapper::class.java, - ), - ArgumentMatchers.eq(defaultMpId), - ) } @Test diff --git a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt index f1b327c9d..1804a3eb8 100644 --- a/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt +++ b/android-core/src/test/kotlin/com/mparticle/internal/KitFrameworkWrapperTest.kt @@ -29,7 +29,6 @@ import org.powermock.modules.junit4.PowerMockRunner import java.lang.ref.WeakReference import java.util.Random import kotlin.test.assertEquals -import kotlin.test.assertNull @RunWith(PowerMockRunner::class) class KitFrameworkWrapperTest { @@ -721,49 +720,4 @@ class KitFrameworkWrapperTest { verify(mockKitManager).setWrapperSdkVersion(expectedSdkVersion) } - - @Test - fun testGetRoktKitApi_kitManagerNull_returnsNull() { - val wrapper = - KitFrameworkWrapper( - mock( - Context::class.java, - ), - mock(ReportingManager::class.java), - mock(ConfigManager::class.java), - mock(AppStateManager::class.java), - true, - mock(MParticleOptions::class.java), - ) - - val result = wrapper.roktKitApi - - assertNull(result) - } - - @Test - fun testGetRoktKitApi_kitManagerSet_delegatesToKitManager() { - val wrapper = - KitFrameworkWrapper( - mock( - Context::class.java, - ), - mock(ReportingManager::class.java), - mock(ConfigManager::class.java), - mock(AppStateManager::class.java), - true, - mock(MParticleOptions::class.java), - ) - - val mockKitManager = mock(KitManager::class.java) - val mockRoktKitApi = mock(RoktKitApi::class.java) - - `when`(mockKitManager.roktKitApi).thenReturn(mockRoktKitApi) - wrapper.setKitManager(mockKitManager) - - val result = wrapper.roktKitApi - - verify(mockKitManager).roktKitApi - assertEquals(mockRoktKitApi, result) - } } diff --git a/android-kit-base/build.gradle b/android-kit-base/build.gradle index 2a1cdcc97..846da389c 100644 --- a/android-kit-base/build.gradle +++ b/android-kit-base/build.gradle @@ -1,11 +1,12 @@ -ext { - kitDescription = 'mParticle Kit library for extending the Core mParticle SDK.' -} - apply plugin: 'com.android.library' -apply from: '../scripts/maven.gradle' +apply plugin: 'mparticle.android.library.publish' apply plugin: 'kotlin-android' +mparticleMavenPublish { + artifactId.set('android-kit-base') + description.set('mParticle Kit library for extending the Core mParticle SDK.') +} + android { namespace 'com.mparticle.kits' @@ -49,36 +50,12 @@ tasks.withType(Test).configureEach { jvmArgs('--add-opens=java.base/java.util.concurrent=ALL-UNNAMED') } -task kitSdkJavadocs(type: Javadoc) { - include { - String filePath = it.toString() - filePath.contains('KitIntegration.java') || - filePath.contains('KitUtils') - } - source = android.sourceSets.main.java.srcDirs - source += 'build/generated/source/buildConfig/release/' - classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - title = 'mParticle Android Kit API Reference' - failOnError true - getOptions().setNoTimestamp(true) -} - -task generateJavadocsJar(type: Jar, dependsOn: kitSdkJavadocs) { - archiveClassifier.set('javadoc') - from kitSdkJavadocs.destinationDir -} - -task generateSourcesJar(type: Jar) { - archiveClassifier.set('sources') - from android.sourceSets.main.java.srcDirs -} - dependencies { api project(':android-core') api 'androidx.annotation:annotation:[1.0.0,)' - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_version" - testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" + testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" testImplementation 'junit:junit:4.13.2' testImplementation files('libs/java-json.jar') diff --git a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/DataplanBlockingUserTests.kt b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/DataplanBlockingUserTests.kt index f54545bc2..7a217eef9 100644 --- a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/DataplanBlockingUserTests.kt +++ b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/DataplanBlockingUserTests.kt @@ -11,9 +11,9 @@ import com.mparticle.Utils.randomString import com.mparticle.identity.IdentityApiRequest import com.mparticle.internal.AccessUtils import com.mparticle.kits.DataplanFilterImpl.Companion.getEventsApiName -import com.mparticle.kits.testkits.AttributeListenerTestKit import com.mparticle.kits.testkits.IdentityListenerTestKit import com.mparticle.kits.testkits.ListenerTestKit +import com.mparticle.kits.testkits.ModifyIdentityListenerTestKit import com.mparticle.kits.testkits.UserAttributeListenerTestKit import com.mparticle.testutils.MPLatch import org.junit.Assert.assertEquals @@ -25,7 +25,7 @@ import org.junit.Test import kotlin.random.Random class DataplanBlockingUserTests : BaseKitOptionsTest() { - private lateinit var attributeListenerKitKit: AttributeListenerTestKit + private lateinit var attributeListenerKitKit: ModifyIdentityListenerTestKit private lateinit var identityListenerKitKit: IdentityListenerTestKit private lateinit var userAttributeListenerKitKit: UserAttributeListenerTestKit private lateinit var kitIntegrationTestKits: List @@ -36,7 +36,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { .builder(mContext) .configuration( KitOptions { - addKit(-1, AttributeListenerTestKit::class.java) + addKit(-1, ModifyIdentityListenerTestKit::class.java) addKit(-2, IdentityListenerTestKit::class.java) addKit(-3, UserAttributeListenerTestKit::class.java) }, @@ -44,7 +44,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { startMParticle(it) } attributeListenerKitKit = - MParticle.getInstance()?.getKitInstance(-1) as AttributeListenerTestKit + MParticle.getInstance()?.getKitInstance(-1) as ModifyIdentityListenerTestKit identityListenerKitKit = MParticle.getInstance()?.getKitInstance(-2) as IdentityListenerTestKit userAttributeListenerKitKit = @@ -73,11 +73,11 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { ), ) - userAttributeListenerKitKit.onSetUserAttribute = { key, _, _ -> + userAttributeListenerKitKit.onSetUserAttribute = { key, _ -> assertTrue(allowedAttributes.containsKey(key)) assertFalse(blockedAttributes.containsKey(key)) } - attributeListenerKitKit.setUserAttribute = { key, _ -> + attributeListenerKitKit.setUserAttributeCallback = { key, _ -> assertTrue(allowedAttributes.containsKey(key)) assertFalse(blockedAttributes.containsKey(key)) } @@ -90,7 +90,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { AccessUtils.awaitMessageHandler() kitIntegrationTestKits.forEach { kit -> - assertEquals(kit.name, allowedAttributes, kit.allUserAttributes) + assertEquals(kit.name, allowedAttributes, kit.getCurrentUser()?.userAttributes) } // sanity check to make sure the non-filtered User has the blocked identities assertEquals( @@ -146,13 +146,13 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { count++ } } - userAttributeListenerKitKit.onRemoveUserAttribute = { key, _ -> + userAttributeListenerKitKit.onRemoveUserAttribute = { key -> assertTrue(allowedAttributes.containsKey(key)) assertFalse(blockedAttributes.containsKey(key)) // make sure these are the attributes that are being removed count++ } - attributeListenerKitKit.removeUserAttribute = { + attributeListenerKitKit.removeUserAttributeListener = { assertTrue(allowedAttributes.containsKey(it)) assertFalse(blockedAttributes.containsKey(it)) count++ @@ -174,7 +174,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { assertEquals(count, allowedAttributes.size * 4) kitIntegrationTestKits.forEach { - assertEquals(0, it.allUserAttributes.size) + assertEquals(0, it.getCurrentUser()?.userAttributes?.size ?: 0) } // sanity check to make sure the non-filtered User has the blocked identities assertEquals( @@ -221,7 +221,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { count++ } } - userAttributeListenerKitKit.onSetUserAttributeList = { key, _, _ -> + userAttributeListenerKitKit.onSetUserAttributeList = { key, _ -> assertTrue(allowedAttributes.containsKey(key)) assertFalse(blockedAttributes.containsKey(key)) count++ @@ -241,8 +241,9 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { assertEquals(count, allowedAttributes.size * 4) kitIntegrationTestKits.forEach { kit -> - assertEquals(allowedAttributes.size, kit.allUserAttributes.size) - assertEquals(allowedAttributes.keys, kit.allUserAttributes.keys) + val filtered = kit.getCurrentUser()?.userAttributes + assertEquals(allowedAttributes.size, filtered?.size ?: 0) + assertEquals(allowedAttributes.keys, filtered?.keys) } // sanity check to make sure the non-filtered User has the blocked attributes assertEquals( @@ -292,7 +293,7 @@ class DataplanBlockingUserTests : BaseKitOptionsTest() { } latch.await() kitIntegrationTestKits.forEach { kit -> - assertEquals(allowedIdentities, kit.userIdentities) + assertEquals(allowedIdentities, kit.getCurrentUser()?.userIdentities) } // sanity check to make sure the non-filtered User has the blocked identities assertEquals( diff --git a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/KitManagerImplTests.kt b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/KitManagerImplTests.kt index e98cb5e67..447c72780 100644 --- a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/KitManagerImplTests.kt +++ b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/KitManagerImplTests.kt @@ -7,8 +7,8 @@ import com.mparticle.MParticle import com.mparticle.MParticleOptions import com.mparticle.internal.ConfigManager import com.mparticle.internal.KitManager -import com.mparticle.kits.testkits.AttributeListenerTestKit import com.mparticle.kits.testkits.IdentityListenerTestKit +import com.mparticle.kits.testkits.ModifyIdentityListenerTestKit import com.mparticle.kits.testkits.UserAttributeListenerTestKit import com.mparticle.networking.Matcher import com.mparticle.testutils.MPLatch @@ -22,7 +22,7 @@ class KitManagerImplTests : BaseKitOptionsTest() { @Test fun testKitIntializationViaKitOptions() { KitOptions() - .addKit(1001, AttributeListenerTestKit::class.java) + .addKit(1001, ModifyIdentityListenerTestKit::class.java) .addKit(1002, IdentityListenerTestKit::class.java) .addKit(1003, UserAttributeListenerTestKit::class.java) .let { @@ -35,7 +35,7 @@ class KitManagerImplTests : BaseKitOptionsTest() { fun getKit(kitId: Int) = MParticle.getInstance()?.getKitInstance(kitId) - assertTrue(getKit(1001) is AttributeListenerTestKit) + assertTrue(getKit(1001) is ModifyIdentityListenerTestKit) assertTrue(getKit(1002) is IdentityListenerTestKit) assertTrue(getKit(1003) is UserAttributeListenerTestKit) } @@ -62,10 +62,10 @@ class KitManagerImplTests : BaseKitOptionsTest() { .builder(mContext) .configuration( ConfiguredKitOptions { - addKit(-1, AttributeListenerTestKit::class.java, JSONObject().put("eau", true)) + addKit(-1, ModifyIdentityListenerTestKit::class.java, JSONObject().put("eau", true)) addKit(-2, IdentityListenerTestKit::class.java) addKit(-3, UserAttributeListenerTestKit::class.java) - addKit(-4, AttributeListenerTestKit::class.java, JSONObject().put("eau", true)) + addKit(-4, ModifyIdentityListenerTestKit::class.java, JSONObject().put("eau", true)) addKit(-5, IdentityListenerTestKit::class.java) addKit(-6, UserAttributeListenerTestKit::class.java) }.apply { diff --git a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/AttributeListenerTestKit.kt b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/ModifyIdentityListenerTestKit.kt similarity index 54% rename from android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/AttributeListenerTestKit.kt rename to android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/ModifyIdentityListenerTestKit.kt index e186da9ed..9a7b644b6 100644 --- a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/AttributeListenerTestKit.kt +++ b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/ModifyIdentityListenerTestKit.kt @@ -1,19 +1,24 @@ package com.mparticle.kits.testkits import com.mparticle.MParticle -import com.mparticle.kits.KitIntegration +import com.mparticle.consent.ConsentState +import com.mparticle.kits.KitIntegration.LogoutListener +import com.mparticle.kits.KitIntegration.ModifyIdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener import com.mparticle.kits.ReportingMessage -open class AttributeListenerTestKit : +open class ModifyIdentityListenerTestKit : ListenerTestKit(), - KitIntegration.AttributeListener { - var setUserAttribute: ((attributeKey: String?, attributeValue: String?) -> Unit)? = null - var setUserAttributeList: ((attributeKey: String?, attributeValueList: List?) -> Unit)? = + UserAttributeListener, + ModifyIdentityListener, + LogoutListener { + var setUserAttributeCallback: ((attributeKey: String?, attributeValue: String?) -> Unit)? = null + var setUserAttributeList: ((attributeKey: String?, attributeValueList: List?) -> Unit)? = null var supportsAttributeLists: (() -> Boolean)? = null var setAllUserAttributes: ((userAttributes: Map?, userAttributeLists: Map>?) -> Unit)? = null - var removeUserAttribute: ((key: String?) -> Unit)? = null + var removeUserAttributeListener: ((key: String?) -> Unit)? = null var setUserIdentity: ((identityType: MParticle.IdentityType?, identity: String?) -> Unit)? = null var removeUserIdentity: ((identityType: MParticle.IdentityType?) -> Unit)? = null @@ -21,31 +26,23 @@ open class AttributeListenerTestKit : override fun supportsAttributeLists() = supportsAttributeLists?.invoke() ?: true - override fun setUserAttributeList( - attributeKey: String, - attributeValueList: MutableList, + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, ) { setUserAttributeList?.invoke(attributeKey, attributeValueList) onAttributeReceived?.invoke(attributeKey, attributeValueList) } - override fun setAllUserAttributes( + override fun onSetAllUserAttributes( userAttributes: Map, - userAttributeLists: Map>, + userAttributeLists: Map>, ) { setAllUserAttributes?.invoke(userAttributes, userAttributeLists) userAttributes.forEach { onAttributeReceived?.invoke(it.key, it.value) } userAttributeLists.forEach { onAttributeReceived?.invoke(it.key, it.value) } } - override fun setUserAttribute( - attributeKey: String, - attributeValue: String?, - ) { - setUserAttribute?.invoke(attributeKey, attributeValue) - onAttributeReceived?.invoke(attributeKey, attributeValue) - } - override fun setUserIdentity( identityType: MParticle.IdentityType, identity: String?, @@ -59,10 +56,41 @@ open class AttributeListenerTestKit : onIdentityReceived?.invoke(identityType, null) } - override fun removeUserAttribute(key: String) { - removeUserAttribute?.invoke(key) + override fun onRemoveUserAttribute( + key: String, + ) { + removeUserAttributeListener?.invoke(key) onAttributeReceived?.invoke(key, null) } + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + if (key == null || value == null || value !is String) { + return + } + setUserAttributeCallback?.invoke(key, value) + onAttributeReceived?.invoke(key, value) + } + + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + } + + override fun onSetUserTag( + key: String?, + ) { + } + + override fun onConsentStateUpdated( + oldState: ConsentState?, + newState: ConsentState?, + ) { + } + override fun logout(): List = logout?.invoke() ?: listOf() } diff --git a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/UserAttributeListenerTestKit.kt b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/UserAttributeListenerTestKit.kt index 8c0fcb881..5f8c041a9 100644 --- a/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/UserAttributeListenerTestKit.kt +++ b/android-kit-base/src/androidTest/kotlin/com/mparticle/kits/testkits/UserAttributeListenerTestKit.kt @@ -1,95 +1,86 @@ package com.mparticle.kits.testkits import com.mparticle.consent.ConsentState -import com.mparticle.kits.FilteredMParticleUser import com.mparticle.kits.KitIntegration open class UserAttributeListenerTestKit : ListenerTestKit(), KitIntegration.UserAttributeListener { - var onIncrementUserAttribute: ((key: String?, incrementedBy: Number, value: String?, user: FilteredMParticleUser?) -> Unit)? = + var onIncrementUserAttribute: ((key: String?, incrementedBy: Number, value: String?) -> Unit)? = null - var onRemoveUserAttribute: ((key: String?, user: FilteredMParticleUser?) -> Unit)? = null - var onSetUserAttribute: ((key: String?, value: Any?, user: FilteredMParticleUser?) -> Unit)? = + var onRemoveUserAttribute: ((key: String?) -> Unit)? = null + var onSetUserAttribute: ((key: String?, value: Any?) -> Unit)? = null - var onSetUserTag: ((key: String?, user: FilteredMParticleUser?) -> Unit)? = null - var onSetUserAttributeList: ((attributeKey: String?, attributeValueList: List?, user: FilteredMParticleUser?) -> Unit)? = + var onSetUserTag: ((key: String?) -> Unit)? = null + var onSetUserAttributeList: ((attributeKey: String?, attributeValueList: List?) -> Unit)? = null var onSetAllUserAttributes: ( ( userAttributes: Map?, userAttributeLists: Map?>?, - user: FilteredMParticleUser?, ) -> Unit )? = null var supportsAttributeLists: (() -> Boolean)? = null - var onConsentStateUpdated: ((oldState: ConsentState?, newState: ConsentState?, user: FilteredMParticleUser?) -> Unit)? = + var onConsentStateUpdated: ((oldState: ConsentState?, newState: ConsentState?) -> Unit)? = null override fun onConsentStateUpdated( oldState: ConsentState?, newState: ConsentState?, - user: FilteredMParticleUser?, ) { - onConsentStateUpdated?.invoke(oldState, newState, user) + onConsentStateUpdated?.invoke(oldState, newState) } override fun onSetAllUserAttributes( userAttributes: Map?, userAttributeLists: Map>?, - user: FilteredMParticleUser?, ) { - onSetAllUserAttributes?.invoke(userAttributes, userAttributeLists, user) + onSetAllUserAttributes?.invoke(userAttributes, userAttributeLists) userAttributes?.forEach { onAttributeReceived?.invoke(it.key, it.value) } } override fun onSetUserAttribute( key: String?, value: Any?, - user: FilteredMParticleUser?, ) { - onSetUserAttribute?.invoke(key, value, user) + onSetUserAttribute?.invoke(key, value) onAttributeReceived?.invoke(key, value) - onUserReceived?.invoke(user) + onUserReceived?.invoke(null) } override fun onSetUserTag( key: String?, - user: FilteredMParticleUser?, ) { - onSetUserTag?.invoke(key, user) + onSetUserTag?.invoke(key) onAttributeReceived?.invoke(key, null) - onUserReceived?.invoke(user) + onUserReceived?.invoke(null) } override fun onIncrementUserAttribute( key: String?, incrementedBy: Number, value: String?, - user: FilteredMParticleUser?, ) { - onIncrementUserAttribute?.invoke(key, incrementedBy, value, user) + onIncrementUserAttribute?.invoke(key, incrementedBy, value) onAttributeReceived?.invoke(key, value) - onUserReceived?.invoke(user) + onUserReceived?.invoke(null) } override fun onSetUserAttributeList( attributeKey: String?, - attributeValueList: MutableList?, - user: FilteredMParticleUser?, + attributeValueList: List?, ) { - onSetUserAttributeList?.invoke(attributeKey, attributeValueList, user) + onSetUserAttributeList?.invoke(attributeKey, attributeValueList) onAttributeReceived?.invoke(attributeKey, attributeValueList) - onUserReceived?.invoke(user) + onUserReceived?.invoke(null) } override fun onRemoveUserAttribute( key: String?, - user: FilteredMParticleUser?, ) { - onRemoveUserAttribute?.invoke(key, user) + onRemoveUserAttribute?.invoke(key) onAttributeReceived?.invoke(key, null) - onUserReceived?.invoke(user) + onUserReceived?.invoke(null) } override fun supportsAttributeLists() = supportsAttributeLists?.invoke() ?: true diff --git a/android-kit-base/src/main/java/com/mparticle/kits/FilteredMParticleUser.java b/android-kit-base/src/main/java/com/mparticle/kits/FilteredMParticleUser.java index 359bafd85..b57b6df03 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/FilteredMParticleUser.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/FilteredMParticleUser.java @@ -6,7 +6,6 @@ import com.mparticle.MParticle; import com.mparticle.TypedUserAttributeListener; -import com.mparticle.UserAttributeListener; import com.mparticle.UserAttributeListenerType; import com.mparticle.consent.ConsentState; import com.mparticle.audience.AudienceResponse; @@ -82,20 +81,6 @@ public void onUserAttributesReceived(@NonNull Map userAttributes, @No if (userAttributes == null) { userAttributes = new HashMap<>(); } - if (listener instanceof UserAttributeListener) { - Map stringifiedAttributes = new HashMap<>(); - for (Map.Entry entry : userAttributes.entrySet()) { - if (entry.getValue() != null) { - stringifiedAttributes.put(entry.getKey(), entry.getValue().toString()); - } else { - stringifiedAttributes.put(entry.getKey(), null); - } - } - ((UserAttributeListener) listener).onUserAttributesReceived( - (Map) KitConfiguration.filterAttributes(filters, stringifiedAttributes), - (Map>) KitConfiguration.filterAttributes(filters, userAttributeLists), - mpid); - } if (listener instanceof TypedUserAttributeListener) { ((TypedUserAttributeListener) listener).onUserAttributesReceived( KitConfiguration.filterAttributes(filters, userAttributes), diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java index 206c108f5..14aa17d66 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitIntegration.java @@ -4,7 +4,6 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.graphics.Typeface; import android.location.Location; import android.net.Uri; import android.os.Bundle; @@ -15,27 +14,19 @@ import com.mparticle.BaseEvent; import com.mparticle.MPEvent; import com.mparticle.MParticle; -import com.mparticle.MpRoktEventCallback; -import com.mparticle.RoktEvent; import com.mparticle.WrapperSdkVersion; import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; import com.mparticle.identity.MParticleUser; -import com.mparticle.rokt.PlacementOptions; -import com.mparticle.rokt.RoktConfig; -import com.mparticle.rokt.RoktEmbeddedView; import org.json.JSONObject; import java.lang.ref.WeakReference; import java.math.BigDecimal; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; -import kotlinx.coroutines.flow.Flow; - /** * Base Kit implementation - all Kits must subclass this. */ @@ -119,66 +110,6 @@ public boolean isDisabled(boolean isOptOutEvent) { (getConfiguration().shouldHonorOptOut() && kitManager.isOptedOut() && !isOptOutEvent); } - @Deprecated - public final Map getUserIdentities() { - MParticle instance = MParticle.getInstance(); - if (instance != null) { - MParticleUser user = instance.Identity().getCurrentUser(); - if (user != null) { - Map identities = user.getUserIdentities(); - identities = getKitManager().getDataplanFilter().transformIdentities(identities); - Map filteredIdentities = new HashMap(identities.size()); - for (Map.Entry entry : identities.entrySet()) { - if (getConfiguration().shouldSetIdentity(entry.getKey())) { - filteredIdentities.put(entry.getKey(), entry.getValue()); - } - } - return identities; - } - } - return new HashMap(); - } - - /** - * Retrieve filtered user attributes. Use this method to retrieve user attributes at any time. - * To ensure that filtering is respected, kits must use this method rather than the public API. - *

- * If the KitIntegration implements the {@link AttributeListener} interface and returns true - * for {@link AttributeListener#supportsAttributeLists()}, this method will pass back all attributes - * as they are (as String values or as List<String> values). Otherwise, this method will comma-separate - * the List values and return back all String values. - * - * @return a Map of attributes according to the logic above. - */ - @Deprecated - public final Map getAllUserAttributes() { - MParticle instance = MParticle.getInstance(); - if (instance != null) { - MParticleUser user = instance.Identity().getCurrentUser(); - if (user != null) { - Map userAttributes = user.getUserAttributes(); - if (kitManager != null) { - userAttributes = kitManager.getDataplanFilter().transformUserAttributes(userAttributes); - } - Map attributes = (Map) KitConfiguration.filterAttributes( - getConfiguration().getUserAttributeFilters(), - userAttributes - ); - if ((this instanceof AttributeListener) && ((AttributeListener) this).supportsAttributeLists()) { - return attributes; - } else { - for (Map.Entry entry : attributes.entrySet()) { - if (entry.getValue() instanceof List) { - attributes.put(entry.getKey(), KitUtils.join((List) entry.getValue())); - } - } - return attributes; - } - } - } - return new HashMap(); - } - public final MParticleUser getCurrentUser() { MParticle instance = MParticle.getInstance(); if (instance != null) { @@ -407,33 +338,9 @@ public interface SessionListener { } /** - * Kits should implement this interface when their underlying service has the notion - * of a user with attributes. + * Kits may implement this interface to respond when the mParticle Identity API performs a logout. */ - @Deprecated - public interface AttributeListener { - - void setUserAttribute(String attributeKey, String attributeValue); - - void setUserAttributeList(String attributeKey, List attributeValueList); - - /** - * Indicate to the mParticle Kit framework if this AttributeListener supports attribute-values as lists. - *

- * If an AttributeListener returns false, the setUserAttributeList method will never be called. Instead, setUserAttribute - * will be called with the attribute-value lists combined as a csv. - * - * @return true if this AttributeListener supports attribute values as lists. - */ - boolean supportsAttributeLists(); - - void setAllUserAttributes(Map userAttributes, Map> userAttributeLists); - - void removeUserAttribute(String key); - - void setUserIdentity(MParticle.IdentityType identityType, String identity); - - void removeUserIdentity(MParticle.IdentityType identityType); + public interface LogoutListener { /** * The mParticle SDK exposes a logout API, allowing developers to track an event @@ -443,6 +350,17 @@ public interface AttributeListener { * @return Kits should return a List of ReportingMessages indicating that the logout was processed one or more times, or null if it was not processed */ List logout(); + } + + /** + * Identity forwarding for kits that also receive user attribute callbacks. Implement together with + * {@link UserAttributeListener} when the kit should receive user attribute updates. + */ + public interface ModifyIdentityListener { + + void setUserIdentity(MParticle.IdentityType identityType, String identity); + + void removeUserIdentity(MParticle.IdentityType identityType); } @@ -595,23 +513,66 @@ public interface IdentityListener { } + /** + * Kits should implement this interface to receive user attribute updates, tags, increments, consent changes, + * and full attribute syncs from the mParticle SDK. + */ public interface UserAttributeListener { - void onIncrementUserAttribute(String key, Number incrementedBy, String value, FilteredMParticleUser user); + /** + * Indicate to the mParticle Kit framework if this listener supports attribute-values as lists. + *

+ * If false, list-specific APIs are not used; values are passed via scalar/csv paths instead. + * + * @return true if this listener supports attribute values as lists. + */ + boolean supportsAttributeLists(); - void onRemoveUserAttribute(String key, FilteredMParticleUser user); + /** + * Called when a user attribute is removed for the current user. + * + * @param key attribute key + */ + void onRemoveUserAttribute(String key); + + /** + * Called when a scalar user attribute is set for the current user. + * + * @param key attribute key + * @param value attribute value (may be non-String for some call paths) + */ + void onSetUserAttribute(String key, Object value); + + /** + * Called when a list-valued user attribute is set and {@link #supportsAttributeLists()} returns true. + * + * @param attributeKey attribute key (may be null) + * @param attributeValueList attribute values (may be null) + */ + void onSetUserAttributeList( + @Nullable String attributeKey, + @Nullable List attributeValueList); - void onSetUserAttribute(String key, Object value, FilteredMParticleUser user); + /** + * Called when the full set of user attributes is synchronized for the current user. + * + * @param userAttributes scalar user attributes + * @param userAttributeLists list-valued user attributes when {@link #supportsAttributeLists()} is true; + * otherwise list values may be merged into scalars by the framework + */ + void onSetAllUserAttributes( + Map userAttributes, + Map> userAttributeLists); - void onSetUserTag(String key, FilteredMParticleUser user); + void onIncrementUserAttribute(String key, Number incrementedBy, String value); - void onSetUserAttributeList(String attributeKey, List attributeValueList, FilteredMParticleUser user); + void onSetUserTag(String key); - void onSetAllUserAttributes(Map userAttributes, Map> userAttributeLists, FilteredMParticleUser user); + void onConsentStateUpdated(ConsentState oldState, ConsentState newState); - boolean supportsAttributeLists(); + KitConfiguration getConfiguration(); - void onConsentStateUpdated(ConsentState oldState, ConsentState newState, FilteredMParticleUser user); + String getName(); } public interface BatchListener { @@ -621,28 +582,10 @@ public interface BatchListener { /** * Interface for Rokt Kit implementations. * - *

This interface is internal to kit-base and is bridged to the - * {@link com.mparticle.internal.RoktKitApi} interface via a wrapper implementation - * in {@link KitManagerImpl}. The wrapper handles user resolution and - * attribute preparation before delegating to the kit's methods.

- * - * @see com.mparticle.internal.RoktKitApi + *

This interface is internal to kit-base and currently used only for wrapper SDK + * version propagation.

*/ public interface RoktListener { - - void selectPlacements(@NonNull String viewName, - @NonNull Map attributes, - @Nullable MpRoktEventCallback mpRoktEventCallback, - @Nullable Map> placeHolders, - @Nullable Map> fontTypefaces, - @Nullable FilteredMParticleUser user, - @Nullable RoktConfig config, - @Nullable PlacementOptions options); - - Flow events(@NonNull String identifier); - - void enrichAttributes( - @NonNull Map attributes, @Nullable FilteredMParticleUser user); /** * Set the SDK version of the mParticle SDK. * This should match the value set in MParticle.getWrapperSdkVersion() @@ -650,26 +593,5 @@ void enrichAttributes( * @param wrapperSdkVersion the version of the mParticle SDK */ void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion); - - void purchaseFinalized(@NonNull String placementId, @NonNull String catalogItemId, boolean status); - - void close(); - - /** - * Set the session id to use for the next execute call. - * This is useful for cases where you have a session id from a non-native integration, - * e.g. WebView, and you want the session to be consistent across integrations. - * - * @param sessionId The session id to be set. Must be a non-empty string. - */ - void setSessionId(@NonNull String sessionId); - - /** - * Get the session id to use within a non-native integration e.g. WebView. - * - * @return The session id or null if no session is present. - */ - @Nullable - String getSessionId(); } } diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java index ef151ee9e..4d1400c03 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitManagerImpl.java @@ -19,14 +19,13 @@ import com.mparticle.AttributionError; import com.mparticle.AttributionListener; +import com.mparticle.TypedUserAttributeListener; import com.mparticle.AttributionResult; import com.mparticle.BaseEvent; import com.mparticle.Configuration; import com.mparticle.MPEvent; import com.mparticle.MParticle; import com.mparticle.MParticleOptions; -import com.mparticle.internal.RoktKitApi; -import com.mparticle.UserAttributeListener; import com.mparticle.WrapperSdkVersion; import com.mparticle.commerce.CommerceEvent; import com.mparticle.consent.ConsentState; @@ -39,6 +38,7 @@ import com.mparticle.internal.Logger; import com.mparticle.internal.MPUtility; import com.mparticle.internal.ReportingManager; +import com.mparticle.kits.KitIntegration.LogoutListener; import com.mparticle.kits.mappings.CustomMapping; import com.mparticle.rokt.RoktOptions; @@ -59,7 +59,7 @@ import java.util.TreeMap; import java.util.concurrent.ConcurrentHashMap; -public class KitManagerImpl implements KitManager, AttributionListener, UserAttributeListener, IdentityStateListener { +public class KitManagerImpl implements KitManager, AttributionListener, IdentityStateListener { private static HandlerThread kitHandlerThread; @@ -86,6 +86,27 @@ public class KitManagerImpl implements KitManager, AttributionListener, UserAttr ConcurrentHashMap providers = new ConcurrentHashMap(); + + List activeKits() { + List activeKits = new ArrayList<>(); + for (KitIntegration kit : providers.values()) { + if (!kit.isDisabled()) { + activeKits.add(kit); + } + } + return activeKits; + } + + private List userAttributeListeners() { + List listeners = new ArrayList<>(); + for (KitIntegration kit : activeKits()) { + if (kit instanceof KitIntegration.UserAttributeListener listener) { + listeners.add(listener); + } + } + return listeners; + } + private final Context mContext; public KitManagerImpl(Context context, ReportingManager reportingManager, CoreCallbacks coreCallbacks, MParticleOptions options) { @@ -135,6 +156,11 @@ public int getUserBucket() { return mCoreCallbacks.getUserBucket(); } + @Override + public boolean isEnabled() { + return mCoreCallbacks.isEnabled(); + } + public boolean isOptedOut() { return !mCoreCallbacks.isEnabled(); } @@ -309,8 +335,8 @@ private void initializeKit(KitIntegration activeKit) { } } - if (activeKit instanceof KitIntegration.AttributeListener) { - syncUserIdentities((KitIntegration.AttributeListener) activeKit, activeKit.getConfiguration()); + if (activeKit instanceof KitIntegration.ModifyIdentityListener) { + syncUserIdentities((KitIntegration.ModifyIdentityListener) activeKit, activeKit.getConfiguration()); } MParticle instance = MParticle.getInstance(); @@ -372,12 +398,10 @@ public Object getKitInstance(int kitId) { @Override public void setLocation(Location location) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (!provider.isDisabled()) { - provider.setLocation(location); - mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), true, location); - } + provider.setLocation(location); + mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), true, location); } catch (Exception e) { Logger.warning("Failed to call setLocation for kit: " + provider.getName() + ": " + e.getMessage()); } @@ -386,13 +410,11 @@ public void setLocation(Location location) { @Override public void logNetworkPerformance(String url, long startTime, String method, long length, long bytesSent, long bytesReceived, String requestString, int responseCode) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (!provider.isDisabled()) { - List report = provider.logNetworkPerformance(url, startTime, method, length, bytesSent, bytesReceived, requestString, responseCode); - getReportingManager().logAll(report); - mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), url, startTime, method, length, bytesSent, bytesReceived, requestString, responseCode); - } + List report = provider.logNetworkPerformance(url, startTime, method, length, bytesSent, bytesReceived, requestString, responseCode); + getReportingManager().logAll(report); + mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), url, startTime, method, length, bytesSent, bytesReceived, requestString, responseCode); } catch (Exception e) { Logger.warning("Failed to call logNetworkPerformance for kit: " + provider.getName() + ": " + e.getMessage()); } @@ -463,75 +485,75 @@ public void logEvent(BaseEvent event) { //================================================================================ protected void logCommerceEvent(CommerceEvent event) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (!provider.isDisabled()) { - CommerceEvent filteredEvent = provider.getConfiguration().filterCommerceEvent(event); - if (filteredEvent != null) { - if (provider instanceof KitIntegration.CommerceListener) { - List projectedEvents = CustomMapping.projectEvents( - filteredEvent, - provider.getConfiguration().getCustomMappingList(), - provider.getConfiguration().getDefaultCommerceCustomMapping() - ); - if (projectedEvents != null && projectedEvents.size() > 0) { - ReportingMessage masterMessage = ReportingMessage.fromEvent(provider, filteredEvent); - boolean forwarded = false; - for (int i = 0; i < projectedEvents.size(); i++) { - CustomMapping.ProjectionResult result = projectedEvents.get(i); - List report = null; - String messageType = null; - if (result.getMPEvent() != null) { - MPEvent projectedEvent = projectedEvents.get(i).getMPEvent(); - report = ((KitIntegration.EventListener) provider).logEvent(projectedEvent); - mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), projectedEvent); - messageType = ReportingMessage.MessageType.EVENT; - } else { - CommerceEvent projectedEvent = projectedEvents.get(i).getCommerceEvent(); - report = ((KitIntegration.CommerceListener) provider).logEvent(projectedEvent); - mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), projectedEvent); - messageType = ReportingMessage.MessageType.COMMERCE_EVENT; - } - if (report != null && report.size() > 0) { - forwarded = true; - for (ReportingMessage message : report) { - masterMessage.addProjectionReport( - new ReportingMessage.ProjectionReport(projectedEvents.get(i).getProjectionId(), - messageType, - message.getEventName(), - message.getEventTypeString()) - ); - } - } - } - if (forwarded) { - getReportingManager().log(masterMessage); - } - } else { - List reporting = ((KitIntegration.CommerceListener) provider).logEvent(filteredEvent); - mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(reporting), filteredEvent); - if (reporting != null && reporting.size() > 0) { - getReportingManager().log( - ReportingMessage.fromEvent(provider, filteredEvent) - ); - } - } - } else if (provider instanceof KitIntegration.EventListener) { - List events = CommerceEventUtils.expand(filteredEvent); + CommerceEvent filteredEvent = provider.getConfiguration().filterCommerceEvent(event); + if (filteredEvent != null) { + if (provider instanceof KitIntegration.CommerceListener) { + List projectedEvents = CustomMapping.projectEvents( + filteredEvent, + provider.getConfiguration().getCustomMappingList(), + provider.getConfiguration().getDefaultCommerceCustomMapping() + ); + if (projectedEvents != null && projectedEvents.size() > 0) { + ReportingMessage masterMessage = ReportingMessage.fromEvent(provider, filteredEvent); boolean forwarded = false; - if (events != null) { - for (MPEvent expandedEvent : events) { - List reporting = ((KitIntegration.EventListener) provider).logEvent(expandedEvent); - mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(reporting), expandedEvent); - forwarded = forwarded || (reporting != null && reporting.size() > 0); + for (int i = 0; i < projectedEvents.size(); i++) { + CustomMapping.ProjectionResult result = projectedEvents.get(i); + List report = null; + String messageType = null; + if (result.getMPEvent() != null) { + MPEvent projectedEvent = projectedEvents.get(i).getMPEvent(); + report = ((KitIntegration.EventListener) provider).logEvent(projectedEvent); + mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), projectedEvent); + messageType = ReportingMessage.MessageType.EVENT; + } else { + CommerceEvent projectedEvent = projectedEvents.get(i).getCommerceEvent(); + report = ((KitIntegration.CommerceListener) provider).logEvent(projectedEvent); + mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), projectedEvent); + messageType = ReportingMessage.MessageType.COMMERCE_EVENT; + } + if (report != null && report.size() > 0) { + forwarded = true; + for (ReportingMessage message : report) { + masterMessage.addProjectionReport( + new ReportingMessage.ProjectionReport(projectedEvents.get(i).getProjectionId(), + messageType, + message.getEventName(), + message.getEventTypeString()) + ); + } } } if (forwarded) { + getReportingManager().log( + masterMessage + ); + } + } else { + List reporting = ((KitIntegration.CommerceListener) provider).logEvent(filteredEvent); + mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(reporting), filteredEvent); + if (reporting != null && reporting.size() > 0) { getReportingManager().log( ReportingMessage.fromEvent(provider, filteredEvent) ); } } + } else if (provider instanceof KitIntegration.EventListener) { + List events = CommerceEventUtils.expand(filteredEvent); + boolean forwarded = false; + if (events != null) { + for (MPEvent expandedEvent : events) { + List reporting = ((KitIntegration.EventListener) provider).logEvent(expandedEvent); + mCoreCallbacks.getKitListener().onKitApiCalled("logMPEvent()", provider.getConfiguration().getKitId(), !MPUtility.isEmpty(reporting), expandedEvent); + forwarded = forwarded || (reporting != null && reporting.size() > 0); + } + } + if (forwarded) { + getReportingManager().log( + ReportingMessage.fromEvent(provider, filteredEvent) + ); + } } } } catch (Exception e) { @@ -546,19 +568,17 @@ protected void logCommerceEvent(CommerceEvent event) { @Override public boolean onMessageReceived(Context context, Intent intent) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { if (provider instanceof KitIntegration.PushListener) { try { - if (!provider.isDisabled()) { - boolean willHandlePush = ((KitIntegration.PushListener) provider).willHandlePushMessage(intent); - mCoreCallbacks.getKitListener().onKitApiCalled("willHandlePushMessage()", provider.getConfiguration().getKitId(), willHandlePush, intent); - if (willHandlePush) { - ((KitIntegration.PushListener) provider).onPushMessageReceived(context, intent); - mCoreCallbacks.getKitListener().onKitApiCalled("onPushMessageReceived()", provider.getConfiguration().getKitId(), null, context, intent); - ReportingMessage message = ReportingMessage.fromPushMessage(provider, intent); - getReportingManager().log(message); - return true; - } + boolean willHandlePush = ((KitIntegration.PushListener) provider).willHandlePushMessage(intent); + mCoreCallbacks.getKitListener().onKitApiCalled("willHandlePushMessage()", provider.getConfiguration().getKitId(), willHandlePush, intent); + if (willHandlePush) { + ((KitIntegration.PushListener) provider).onPushMessageReceived(context, intent); + mCoreCallbacks.getKitListener().onKitApiCalled("onPushMessageReceived()", provider.getConfiguration().getKitId(), null, context, intent); + ReportingMessage message = ReportingMessage.fromPushMessage(provider, intent); + getReportingManager().log(message); + return true; } } catch (Exception e) { Logger.warning("Failed to call onPushMessageReceived for kit: " + provider.getName() + ": " + e.getMessage()); @@ -570,18 +590,16 @@ public boolean onMessageReceived(Context context, Intent intent) { @Override public boolean onPushRegistration(String token, String senderId) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { if (provider instanceof KitIntegration.PushListener) { try { - if (!provider.isDisabled()) { - boolean onPushRegistration = ((KitIntegration.PushListener) provider).onPushRegistration(token, senderId); - mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), onPushRegistration, token, senderId); - if (onPushRegistration) { - ReportingMessage message = ReportingMessage.fromPushRegistrationMessage(provider); - getReportingManager().log(message); - } - return true; + boolean onPushRegistration = ((KitIntegration.PushListener) provider).onPushRegistration(token, senderId); + mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), onPushRegistration, token, senderId); + if (onPushRegistration) { + ReportingMessage message = ReportingMessage.fromPushRegistrationMessage(provider); + getReportingManager().log(message); } + return true; } catch (Exception e) { Logger.warning("Failed to call onPushRegistration for kit: " + provider.getName() + ": " + e.getMessage()); } @@ -591,62 +609,49 @@ public boolean onPushRegistration(String token, String senderId) { } //================================================================================ - // KitIntegration.AttributeListener forwarding + // UserAttributeListener user-attribute forwarding //================================================================================ - @Override public void onUserAttributesReceived(Map userAttributes, Map> userAttributeLists, Long mpid) { userAttributes = mDataplanFilter.transformUserAttributes(userAttributes); userAttributeLists = mDataplanFilter.transformUserAttributes(userAttributeLists); - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - if ((provider instanceof KitIntegration.AttributeListener || provider instanceof KitIntegration.UserAttributeListener) - && !provider.isDisabled()) { - Map filteredAttributeSingles = (Map) KitConfiguration.filterAttributes(provider.getConfiguration().getUserAttributeFilters(), - userAttributes); - Map> filteredAttributeLists = (Map>) KitConfiguration.filterAttributes(provider.getConfiguration().getUserAttributeFilters(), - userAttributeLists); - if (provider instanceof KitIntegration.AttributeListener) { - if (((KitIntegration.AttributeListener) provider).supportsAttributeLists()) { - ((KitIntegration.AttributeListener) provider).setAllUserAttributes(filteredAttributeSingles, filteredAttributeLists); - } else { - Map singlesCopy = new HashMap<>(filteredAttributeSingles); - for (Map.Entry> entry : filteredAttributeLists.entrySet()) { - singlesCopy.put(entry.getKey(), KitUtils.join(entry.getValue())); - } - ((KitIntegration.AttributeListener) provider).setAllUserAttributes(singlesCopy, new HashMap>()); - } - } - if (provider instanceof KitIntegration.UserAttributeListener) { - if (((KitIntegration.UserAttributeListener) provider).supportsAttributeLists()) { - ((KitIntegration.UserAttributeListener) provider).onSetAllUserAttributes(filteredAttributeSingles, filteredAttributeLists, FilteredMParticleUser.getInstance(mpid, provider)); - } else { - Map singlesCopy = new HashMap<>(filteredAttributeSingles); - for (Map.Entry> entry : filteredAttributeLists.entrySet()) { - singlesCopy.put(entry.getKey(), KitUtils.join(entry.getValue())); - } - ((KitIntegration.UserAttributeListener) provider).onSetAllUserAttributes(singlesCopy, new HashMap>(), FilteredMParticleUser.getInstance(mpid, provider)); - } + Map filteredAttributeSingles = (Map) KitConfiguration.filterAttributes(listener.getConfiguration().getUserAttributeFilters(), + userAttributes); + Map> filteredAttributeLists = (Map>) KitConfiguration.filterAttributes(listener.getConfiguration().getUserAttributeFilters(), + userAttributeLists); + boolean supportsAttributeLists = listener.supportsAttributeLists(); + if (supportsAttributeLists) { + listener.onSetAllUserAttributes(filteredAttributeSingles, filteredAttributeLists); + } else { + Map singlesCopy = new HashMap<>(filteredAttributeSingles); + for (Map.Entry> entry : filteredAttributeLists.entrySet()) { + singlesCopy.put(entry.getKey(), KitUtils.join(entry.getValue())); } + listener.onSetAllUserAttributes(singlesCopy, new HashMap>()); } } catch (Exception e) { - Logger.warning("Failed to call setUserAttributes for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call setUserAttributes for kit: " + listener.getName() + ": " + e.getMessage()); } } } - private void syncUserIdentities(KitIntegration.AttributeListener attributeListener, KitConfiguration configuration) { + private void syncUserIdentities(KitIntegration.ModifyIdentityListener listener, KitConfiguration configuration) { MParticle instance = MParticle.getInstance(); - if (instance != null) { - MParticleUser user = instance.Identity().getCurrentUser(); - if (user != null) { - Map identities = user.getUserIdentities(); - if (identities != null) { - for (Map.Entry entry : identities.entrySet()) { - if (configuration.shouldSetIdentity(entry.getKey())) { - attributeListener.setUserIdentity(entry.getKey(), entry.getValue()); - } - } - } + if (instance == null) { + return; + } + + MParticleUser user = instance.Identity().getCurrentUser(); + if (user == null) { + return; + } + + Map identities = user.getUserIdentities(); + + for (Map.Entry entry : identities.entrySet()) { + if (configuration.shouldSetIdentity(entry.getKey())) { + listener.setUserIdentity(entry.getKey(), entry.getValue()); } } } @@ -656,11 +661,14 @@ public void setUserAttribute(String attributeKey, String attributeValue, long mp if (mDataplanFilter.isUserAttributeBlocked(attributeKey)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - setUserAttribute(provider, attributeKey, attributeValue, mpid); + if (KitConfiguration.shouldForwardAttribute(listener.getConfiguration().getUserAttributeFilters(), + attributeKey)) { + listener.onSetUserAttribute(attributeKey, attributeValue); + } } catch (Exception e) { - Logger.warning("Failed to call setUserAttributes/onSetUserAttribute for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call setUserAttributes/onSetUserAttribute for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -670,46 +678,18 @@ public void setUserAttributeList(String attributeKey, List valuesList, l if (mDataplanFilter.isUserAttributeBlocked(attributeKey)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - setUserAttribute(provider, attributeKey, valuesList, mpid); - } catch (Exception e) { - Logger.warning("Failed to call setUserAttributes/onSetUserAttribute for kit: " + provider.getName() + ": " + e.getMessage()); - } - } - } - - private void setUserAttribute(KitIntegration provider, String attributeKey, List valueList, long mpid) { - if ((provider instanceof KitIntegration.AttributeListener || provider instanceof KitIntegration.UserAttributeListener) - && !provider.isDisabled() - && KitConfiguration.shouldForwardAttribute(provider.getConfiguration().getUserAttributeFilters(), attributeKey)) { - if (provider instanceof KitIntegration.AttributeListener) { - if (((KitIntegration.AttributeListener) provider).supportsAttributeLists()) { - ((KitIntegration.AttributeListener) provider).setUserAttributeList(attributeKey, valueList); - } else { - ((KitIntegration.AttributeListener) provider).setUserAttribute(attributeKey, KitUtils.join(valueList)); - } - } - if (provider instanceof KitIntegration.UserAttributeListener) { - if (((KitIntegration.UserAttributeListener) provider).supportsAttributeLists()) { - ((KitIntegration.UserAttributeListener) provider).onSetUserAttributeList(attributeKey, valueList, FilteredMParticleUser.getInstance(mpid, provider)); - } else { - ((KitIntegration.UserAttributeListener) provider).onSetUserAttribute(attributeKey, KitUtils.join(valueList), FilteredMParticleUser.getInstance(mpid, provider)); + if (KitConfiguration.shouldForwardAttribute(listener.getConfiguration().getUserAttributeFilters(), attributeKey)) { + boolean supportsAttributeLists = listener.supportsAttributeLists(); + if (supportsAttributeLists) { + listener.onSetUserAttributeList(attributeKey, valuesList); + } else { + listener.onSetUserAttribute(attributeKey, KitUtils.join(valuesList)); + } } - } - } - } - - private void setUserAttribute(KitIntegration provider, String attributeKey, String attributeValue, long mpid) { - if ((provider instanceof KitIntegration.AttributeListener || provider instanceof KitIntegration.UserAttributeListener) - && !provider.isDisabled() - && KitConfiguration.shouldForwardAttribute(provider.getConfiguration().getUserAttributeFilters(), - attributeKey)) { - if (provider instanceof KitIntegration.AttributeListener) { - ((KitIntegration.AttributeListener) provider).setUserAttribute(attributeKey, attributeValue); - } - if (provider instanceof KitIntegration.UserAttributeListener) { - ((KitIntegration.UserAttributeListener) provider).onSetUserAttribute(attributeKey, attributeValue, FilteredMParticleUser.getInstance(mpid, provider)); + } catch (Exception e) { + Logger.warning("Failed to call setUserAttributes/onSetUserAttribute for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -719,20 +699,13 @@ public void removeUserAttribute(String key, long mpid) { if (mDataplanFilter.isUserAttributeBlocked(key)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - if ((provider instanceof KitIntegration.AttributeListener || provider instanceof KitIntegration.UserAttributeListener) - && !provider.isDisabled() - && KitConfiguration.shouldForwardAttribute(provider.getConfiguration().getUserAttributeFilters(), key)) { - if (provider instanceof KitIntegration.AttributeListener) { - ((KitIntegration.AttributeListener) provider).removeUserAttribute(key); - } - if (provider instanceof KitIntegration.UserAttributeListener) { - ((KitIntegration.UserAttributeListener) provider).onRemoveUserAttribute(key, FilteredMParticleUser.getInstance(mpid, provider)); - } + if (KitConfiguration.shouldForwardAttribute(listener.getConfiguration().getUserAttributeFilters(), key)) { + listener.onRemoveUserAttribute(key); } } catch (Exception e) { - Logger.warning("Failed to call removeUserAttribute/onRemoveUserAttribute for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call removeUserAttribute/onRemoveUserAttribute for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -742,17 +715,14 @@ public void incrementUserAttribute(String key, Number incrementedBy, String newV if (mDataplanFilter.isUserAttributeBlocked(key)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - if (!provider.isDisabled() && KitConfiguration.shouldForwardAttribute(provider.getConfiguration().getUserAttributeFilters(), key)) - if (provider instanceof KitIntegration.UserAttributeListener) { - ((KitIntegration.UserAttributeListener) provider).onIncrementUserAttribute(key, incrementedBy, newValue, FilteredMParticleUser.getInstance(mpid, provider)); - } - if (provider instanceof KitIntegration.AttributeListener) { - ((KitIntegration.AttributeListener) provider).setUserAttribute(key, newValue); + if (KitConfiguration.shouldForwardAttribute(listener.getConfiguration().getUserAttributeFilters(), key)) { + listener.onIncrementUserAttribute(key, incrementedBy, newValue); + listener.onSetUserAttribute(key, newValue); } } catch (Exception e) { - Logger.warning("Failed to call onIncrementUserAttribute for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call onIncrementUserAttribute for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -762,14 +732,13 @@ public void setUserTag(String tag, long mpid) { if (mDataplanFilter.isUserAttributeBlocked(tag)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { try { - if (provider instanceof KitIntegration.UserAttributeListener && !provider.isDisabled() - && KitConfiguration.shouldForwardAttribute(provider.getConfiguration().getUserAttributeFilters(), tag)) { - ((KitIntegration.UserAttributeListener) provider).onSetUserTag(tag, FilteredMParticleUser.getInstance(mpid, provider)); + if (KitConfiguration.shouldForwardAttribute(listener.getConfiguration().getUserAttributeFilters(), tag)) { + listener.onSetUserTag(tag); } } catch (Exception e) { - Logger.warning("Failed to call onSetUserTag for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call onSetUserTag for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -779,13 +748,13 @@ public void setUserIdentity(String id, MParticle.IdentityType identityType) { if (mDataplanFilter.isUserIdentityBlocked(identityType)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration kit : activeKits()) { try { - if (provider instanceof KitIntegration.AttributeListener && !provider.isDisabled() && provider.getConfiguration().shouldSetIdentity(identityType)) { - ((KitIntegration.AttributeListener) provider).setUserIdentity(identityType, id); + if (kit instanceof KitIntegration.ModifyIdentityListener listener && kit.getConfiguration().shouldSetIdentity(identityType)) { + listener.setUserIdentity(identityType, id); } } catch (Exception e) { - Logger.warning("Failed to call setUserIdentity for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call setUserIdentity for kit: " + kit.getName() + ": " + e.getMessage()); } } } @@ -795,27 +764,26 @@ public void removeUserIdentity(MParticle.IdentityType identityType) { if (mDataplanFilter.isUserIdentityBlocked(identityType)) { return; } - for (KitIntegration provider : providers.values()) { + for (KitIntegration kit : activeKits()) { try { - if (provider instanceof KitIntegration.AttributeListener && !provider.isDisabled()) { - ((KitIntegration.AttributeListener) provider).removeUserIdentity(identityType); + if (kit instanceof KitIntegration.ModifyIdentityListener listener) { + listener.removeUserIdentity(identityType); } } catch (Exception e) { - Logger.warning("Failed to call removeUserIdentity for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call removeUserIdentity for kit: " + kit.getName() + ": " + e.getMessage()); } } } @Override public void logout() { - for (KitIntegration provider : providers.values()) { + for (KitIntegration kit : activeKits()) { try { - if (provider instanceof KitIntegration.AttributeListener && !provider.isDisabled()) { - List report = ((KitIntegration.AttributeListener) provider).logout(); - getReportingManager().logAll(report); + if (kit instanceof LogoutListener listener) { + getReportingManager().logAll(listener.logout()); } } catch (Exception e) { - Logger.warning("Failed to call logout for kit: " + provider.getName() + ": " + e.getMessage()); + Logger.warning("Failed to call logout for kit: " + kit.getName() + ": " + e.getMessage()); } } } @@ -921,10 +889,10 @@ public void logBatch(String batch) { @Override public void leaveBreadcrumb(String breadcrumb) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.EventListener && !provider.isDisabled()) { - List report = ((KitIntegration.EventListener) provider).leaveBreadcrumb(breadcrumb); + if (provider instanceof KitIntegration.EventListener listener) { + List report = listener.leaveBreadcrumb(breadcrumb); getReportingManager().logAll(report); mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), breadcrumb); } @@ -936,10 +904,10 @@ public void leaveBreadcrumb(String breadcrumb) { @Override public void logError(String message, Map eventData) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.EventListener && !provider.isDisabled()) { - List report = ((KitIntegration.EventListener) provider).logError(message, eventData); + if (provider instanceof KitIntegration.EventListener listener) { + List report = listener.logError(message, eventData); getReportingManager().logAll(report); mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), message, eventData); } @@ -951,10 +919,10 @@ public void logError(String message, Map eventData) { @Override public void logException(Exception exception, Map eventData, String message) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.EventListener && !provider.isDisabled()) { - List report = ((KitIntegration.EventListener) provider).logException(exception, eventData, message); + if (provider instanceof KitIntegration.EventListener listener) { + List report = listener.logException(exception, eventData, message); getReportingManager().logAll(report); mCoreCallbacks.getKitListener().onKitApiCalled(provider.getConfiguration().getKitId(), !MPUtility.isEmpty(report), exception, message, eventData); } @@ -1038,10 +1006,10 @@ public void logScreen(MPEvent screenEvent) { @Override public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityCreated(activity, savedInstanceState); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityCreated(activity, savedInstanceState); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1052,10 +1020,10 @@ public void onActivityCreated(Activity activity, Bundle savedInstanceState) { @Override public void onActivityStarted(Activity activity) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityStarted(activity); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityStarted(activity); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1066,10 +1034,10 @@ public void onActivityStarted(Activity activity) { @Override public void onActivityResumed(Activity activity) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityResumed(activity); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityResumed(activity); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1080,10 +1048,10 @@ public void onActivityResumed(Activity activity) { @Override public void onActivityPaused(Activity activity) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityPaused(activity); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityPaused(activity); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1094,10 +1062,10 @@ public void onActivityPaused(Activity activity) { @Override public void onActivityStopped(Activity activity) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityStopped(activity); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityStopped(activity); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1108,10 +1076,10 @@ public void onActivityStopped(Activity activity) { @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivitySaveInstanceState(activity, outState); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivitySaveInstanceState(activity, outState); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1122,10 +1090,10 @@ public void onActivitySaveInstanceState(Activity activity, Bundle outState) { @Override public void onActivityDestroyed(Activity activity) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.ActivityListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.ActivityListener) provider).onActivityDestroyed(activity); + if (provider instanceof KitIntegration.ActivityListener listener) { + List reportingMessages = listener.onActivityDestroyed(activity); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1136,10 +1104,10 @@ public void onActivityDestroyed(Activity activity) { @Override public void onSessionEnd() { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.SessionListener && !provider.isDisabled()) { - List reportingMessages = ((KitIntegration.SessionListener) provider).onSessionEnd(); + if (provider instanceof KitIntegration.SessionListener listener) { + List reportingMessages = listener.onSessionEnd(); getReportingManager().logAll(reportingMessages); } } catch (Exception e) { @@ -1150,9 +1118,9 @@ public void onSessionEnd() { @Override public void onSessionStart() { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.SessionListener && !provider.isDisabled()) { + if (provider instanceof KitIntegration.SessionListener) { List reportingMessages = ((KitIntegration.SessionListener) provider).onSessionStart(); getReportingManager().logAll(reportingMessages); } @@ -1226,11 +1194,9 @@ public void installReferrerUpdated() { MParticle instance = MParticle.getInstance(); if (instance != null) { Intent mockIntent = getMockInstallReferrerIntent(instance.getInstallReferrer()); - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (!provider.isDisabled()) { - provider.setInstallReferrer(mockIntent); - } + provider.setInstallReferrer(mockIntent); } catch (Exception e) { Logger.warning("Failed to update Install Referrer for kit: " + provider.getName() + ": " + e.getMessage()); } @@ -1245,25 +1211,38 @@ public void installReferrerUpdated() { public void onUserIdentified(MParticleUser mParticleUser, MParticleUser previousUser) { //due to consent forwarding rules we need to re-verify kits whenever the user changes reloadKits(); - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.IdentityListener && !provider.isDisabled()) { - ((KitIntegration.IdentityListener) provider).onUserIdentified(FilteredMParticleUser.getInstance(mParticleUser, provider)); + if (provider instanceof KitIntegration.IdentityListener listener) { + listener.onUserIdentified(FilteredMParticleUser.getInstance(mParticleUser, provider)); } } catch (Exception e) { Logger.warning("Failed to call onUserIdentified for kit: " + provider.getName() + ": " + e.getMessage()); } } - mParticleUser.getUserAttributes(this); + mParticleUser.getUserAttributes(new TypedUserAttributeListener() { + @Override + public void onUserAttributesReceived(@NonNull Map userAttributes, @NonNull Map> userAttributeLists, long mpid) { + Map stringAttributes = new HashMap<>(); + for (Map.Entry entry : userAttributes.entrySet()) { + if (entry.getValue() != null) { + stringAttributes.put(entry.getKey(), entry.getValue().toString()); + } else { + stringAttributes.put(entry.getKey(), null); + } + } + KitManagerImpl.this.onUserAttributesReceived(stringAttributes, (Map>) userAttributeLists, mpid); + } + }); } @Override public void onIdentifyCompleted(MParticleUser mParticleUser, IdentityApiRequest identityApiRequest) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.IdentityListener && !provider.isDisabled()) { - ((KitIntegration.IdentityListener) provider).onIdentifyCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); + if (provider instanceof KitIntegration.IdentityListener listener) { + listener.onIdentifyCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); } } catch (Exception e) { Logger.warning("Failed to call onIdentifyCompleted for kit: " + provider.getName() + ": " + e.getMessage()); @@ -1273,10 +1252,10 @@ public void onIdentifyCompleted(MParticleUser mParticleUser, IdentityApiRequest @Override public void onLoginCompleted(MParticleUser mParticleUser, IdentityApiRequest identityApiRequest) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.IdentityListener && !provider.isDisabled()) { - ((KitIntegration.IdentityListener) provider).onLoginCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); + if (provider instanceof KitIntegration.IdentityListener listener) { + listener.onLoginCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); } } catch (Exception e) { Logger.warning("Failed to call onLoginCompleted for kit: " + provider.getName() + ": " + e.getMessage()); @@ -1286,10 +1265,10 @@ public void onLoginCompleted(MParticleUser mParticleUser, IdentityApiRequest ide @Override public void onLogoutCompleted(MParticleUser mParticleUser, IdentityApiRequest identityApiRequest) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.IdentityListener && !provider.isDisabled()) { - ((KitIntegration.IdentityListener) provider).onLogoutCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); + if (provider instanceof KitIntegration.IdentityListener listener) { + listener.onLogoutCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); } } catch (Exception e) { Logger.warning("Failed to call onLogoutCompleted for kit: " + provider.getName() + ": " + e.getMessage()); @@ -1299,10 +1278,10 @@ public void onLogoutCompleted(MParticleUser mParticleUser, IdentityApiRequest id @Override public void onModifyCompleted(MParticleUser mParticleUser, IdentityApiRequest identityApiRequest) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.IdentityListener && !provider.isDisabled()) { - ((KitIntegration.IdentityListener) provider).onModifyCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); + if (provider instanceof KitIntegration.IdentityListener listener) { + listener.onModifyCompleted(FilteredMParticleUser.getInstance(mParticleUser, provider), new FilteredIdentityApiRequest(identityApiRequest, provider)); } } catch (Exception e) { Logger.warning("Failed to call onModifyCompleted for kit: " + provider.getName() + ": " + e.getMessage()); @@ -1314,13 +1293,11 @@ public void onModifyCompleted(MParticleUser mParticleUser, IdentityApiRequest id public void onConsentStateUpdated(final ConsentState oldState, final ConsentState newState, final long mpid) { //Due to consent forwarding rules we need to re-initialize kits whenever the user changes. reloadKits(); - for (KitIntegration provider : providers.values()) { - if (provider instanceof KitIntegration.UserAttributeListener && !provider.isDisabled()) { - try { - ((KitIntegration.UserAttributeListener) provider).onConsentStateUpdated(oldState, newState, FilteredMParticleUser.getInstance(mpid, provider)); - } catch (Exception e) { - Logger.warning("Failed to call onConsentStateUpdated for kit: " + provider.getName() + ": " + e.getMessage()); - } + for (KitIntegration.UserAttributeListener listener : userAttributeListeners()) { + try { + listener.onConsentStateUpdated(oldState, newState); + } catch (Exception e) { + Logger.warning("Failed to call onConsentStateUpdated for kit: " + listener.getName() + ": " + e.getMessage()); } } } @@ -1336,23 +1313,12 @@ public void reset() { } } - @Override - @Nullable - public RoktKitApi getRoktKitApi() { - for (KitIntegration provider : providers.values()) { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - return new RoktKitApiImpl((KitIntegration.RoktListener) provider, provider); - } - } - return null; - } - @Override public void setWrapperSdkVersion(@NonNull WrapperSdkVersion wrapperSdkVersion) { - for (KitIntegration provider : providers.values()) { + for (KitIntegration provider : activeKits()) { try { - if (provider instanceof KitIntegration.RoktListener && !provider.isDisabled()) { - ((KitIntegration.RoktListener) provider).setWrapperSdkVersion(wrapperSdkVersion); + if (provider instanceof KitIntegration.RoktListener listener) { + listener.setWrapperSdkVersion(wrapperSdkVersion); } } catch (Exception e) { Logger.warning("Failed to call setWrapperSdkVersion for kit: " + provider.getName() + ": " + e.getMessage()); diff --git a/android-kit-base/src/main/java/com/mparticle/kits/KitUtils.java b/android-kit-base/src/main/java/com/mparticle/kits/KitUtils.java index 6b7cb4834..aba68c7a5 100644 --- a/android-kit-base/src/main/java/com/mparticle/kits/KitUtils.java +++ b/android-kit-base/src/main/java/com/mparticle/kits/KitUtils.java @@ -166,7 +166,7 @@ public static boolean parseBooleanSetting(Map settings, String k @TargetApi(Build.VERSION_CODES.CUPCAKE) @Nullable public static String getAndroidID(Context context) { - if (!MParticle.isAndroidIdDisabled()) { + if (MParticle.isAndroidIdEnabled()) { return Settings.Secure.getString(context.getContentResolver(), "android_id"); } else { return null; diff --git a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt b/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt deleted file mode 100644 index 138ca9c75..000000000 --- a/android-kit-base/src/main/kotlin/com/mparticle/kits/RoktKitApiImpl.kt +++ /dev/null @@ -1,288 +0,0 @@ -package com.mparticle.kits - -import android.graphics.Typeface -import com.mparticle.MParticle -import com.mparticle.MpRoktEventCallback -import com.mparticle.RoktEvent -import com.mparticle.TypedUserAttributeListener -import com.mparticle.identity.IdentityApi -import com.mparticle.identity.IdentityApiRequest -import com.mparticle.identity.MParticleUser -import com.mparticle.internal.Constants -import com.mparticle.internal.Logger -import com.mparticle.internal.MPUtility -import com.mparticle.internal.RoktKitApi -import com.mparticle.rokt.PlacementOptions -import com.mparticle.rokt.RoktConfig -import com.mparticle.rokt.RoktEmbeddedView -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import org.json.JSONException -import java.lang.ref.WeakReference -import java.util.Objects - -/** - * Implementation of [RoktKitApi] that wraps a [KitIntegration.RoktListener]. - * - * This class handles user resolution and attribute preparation before delegating - * to the underlying Rokt Kit implementation. - */ -internal class RoktKitApiImpl(private val roktListener: KitIntegration.RoktListener, private val kitIntegration: KitIntegration) : RoktKitApi { - - override fun selectPlacements( - viewName: String, - attributes: Map, - mpRoktEventCallback: MpRoktEventCallback?, - placeHolders: Map>?, - fontTypefaces: Map>?, - config: RoktConfig?, - options: PlacementOptions?, - ) { - try { - val mutableAttributes = attributes.toMutableMap() - val instance = MParticle.getInstance() - if (instance == null) { - Logger.warning("MParticle instance is null, cannot execute Rokt placement") - return - } - val user = instance.Identity().currentUser - val email = getValueIgnoreCase(mutableAttributes, "email") - val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256") - val kitConfig = kitIntegration.configuration - - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) { - val finalAttributes = applyPlacementAttributeMapping(mutableAttributes) - setRoktAttributesOnUser(finalAttributes, user) { - ensureSandboxMode(finalAttributes) - roktListener.selectPlacements( - viewName, - finalAttributes, - mpRoktEventCallback, - placeHolders, - fontTypefaces, - FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration), - config, - options, - ) - } - } - } catch (e: Exception) { - Logger.warning("Failed to call execute for Rokt Kit: ${e.message}") - } - } - - override fun events(identifier: String): Flow = try { - Logger.verbose("Calling events for Rokt Kit with identifier: $identifier") - roktListener.events(identifier) - } catch (e: Exception) { - Logger.warning("Failed to call events for Rokt Kit: ${e.message}") - flowOf() - } - - override fun purchaseFinalized(placementId: String, catalogItemId: String, status: Boolean) { - try { - roktListener.purchaseFinalized(placementId, catalogItemId, status) - } catch (e: Exception) { - Logger.warning("Failed to call purchaseFinalized for Rokt Kit: ${e.message}") - } - } - - override fun close() { - try { - roktListener.close() - } catch (e: Exception) { - Logger.warning("Failed to call close for Rokt Kit: ${e.message}") - } - } - - override fun setSessionId(sessionId: String) { - try { - roktListener.setSessionId(sessionId) - } catch (e: Exception) { - Logger.warning("Failed to call setSessionId for Rokt Kit: ${e.message}") - } - } - - override fun getSessionId(): String? = try { - roktListener.sessionId - } catch (e: Exception) { - Logger.warning("Failed to call getSessionId for Rokt Kit: ${e.message}") - null - } - - override fun prepareAttributesAsync(attributes: Map) { - try { - val mutableAttributes = attributes.toMutableMap() - val instance = MParticle.getInstance() - if (instance == null) { - Logger.warning("MParticle instance is null, cannot prepare attributes") - return - } - val user = instance.Identity().currentUser - val email = getValueIgnoreCase(mutableAttributes, "email") - val hashedEmail = getValueIgnoreCase(mutableAttributes, "emailsha256") - val kitConfig = kitIntegration.configuration - - confirmEmail(email, hashedEmail, user, instance.Identity(), kitConfig) { - val finalAttributes = applyPlacementAttributeMapping(mutableAttributes) - setRoktAttributesOnUser(finalAttributes, user) { - ensureSandboxMode(finalAttributes) - roktListener.enrichAttributes( - finalAttributes, - FilteredMParticleUser.getInstance(user?.id ?: 0L, kitIntegration), - ) - } - } - } catch (e: Exception) { - Logger.warning("Failed to call prepareAttributesAsync for Rokt Kit: ${e.message}") - } - } - - // Helper methods - - private fun getValueIgnoreCase(map: Map, searchKey: String): String? { - for ((key, value) in map) { - if (key.equals(searchKey, ignoreCase = true)) { - return value - } - } - return null - } - - private fun applyPlacementAttributeMapping(attributes: MutableMap): MutableMap { - val kitConfig = kitIntegration.configuration - val jsonArray = try { - kitConfig?.placementAttributesMapping ?: org.json.JSONArray() - } catch (e: JSONException) { - Logger.warning("Invalid placementAttributes for Rokt Kit JSON: ${e.message}") - org.json.JSONArray() - } - - for (i in 0 until jsonArray.length()) { - val obj = jsonArray.optJSONObject(i) ?: continue - val mapFrom = obj.optString("map") - val mapTo = obj.optString("value") - if (attributes.containsKey(mapFrom)) { - val value = attributes.remove(mapFrom) - if (value != null) { - attributes[mapTo] = value - } - } - } - return attributes - } - - private fun ensureSandboxMode(finalAttributes: MutableMap) { - if (!finalAttributes.containsKey(Constants.MessageKey.SANDBOX_MODE_ROKT)) { - finalAttributes[Constants.MessageKey.SANDBOX_MODE_ROKT] = - Objects.toString(MPUtility.isDevEnv(), "false") - } - } - - /** - * Persists placement attributes on the mParticle user, then invokes [onReady] after the - * attributes have been applied. This ensures the Rokt kit reads an up-to-date user profile - * when merging attributes for the placement. - */ - private fun setRoktAttributesOnUser(finalAttributes: Map, user: MParticleUser?, onReady: Runnable) { - val objectAttributes = mutableMapOf() - for ((key, value) in finalAttributes) { - if (key != Constants.MessageKey.SANDBOX_MODE_ROKT) { - objectAttributes[key] = value - } - } - if (user != null) { - user.setUserAttributes(objectAttributes) - user.getUserAttributes( - object : TypedUserAttributeListener { - override fun onUserAttributesReceived( - userAttributes: Map, - userAttributeLists: Map?>, - mpid: Long, - ) { - onReady.run() - } - }, - ) - } else { - onReady.run() - } - } - - private fun confirmEmail( - email: String?, - hashedEmail: String?, - user: MParticleUser?, - identityApi: IdentityApi, - kitConfiguration: KitConfiguration?, - runnable: Runnable, - ) { - val hasEmail = !email.isNullOrEmpty() - val hasHashedEmail = !hashedEmail.isNullOrEmpty() - - if ((hasEmail || hasHashedEmail) && user != null) { - var selectedIdentityType: MParticle.IdentityType? = null - try { - val identityTypeStr = kitConfiguration?.hashedEmailUserIdentityType - if (identityTypeStr != null) { - selectedIdentityType = MParticle.IdentityType.valueOf(identityTypeStr) - } - } catch (e: IllegalArgumentException) { - Logger.error("Invalid identity type ${e.message}") - } - - val existingEmail = user.userIdentities[MParticle.IdentityType.Email] - val existingHashedEmail = selectedIdentityType?.let { user.userIdentities[it] } - val emailMismatch = hasEmail && !email.equals(existingEmail, ignoreCase = true) - val hashedEmailMismatch = - hasHashedEmail && !hashedEmail.equals(existingHashedEmail, ignoreCase = true) - - if (emailMismatch || (hashedEmailMismatch && selectedIdentityType != null)) { - // If there's an existing email but it doesn't match the passed-in email, log a warning - if (emailMismatch && existingEmail != null) { - Logger.warning( - "The existing email on the user ($existingEmail) does not match the email passed to selectPlacements ($email). " + - "Please make sure to sync the email identity to mParticle as soon as it's available. " + - "Identifying user with the provided email before continuing to selectPlacements.", - ) - } else if (hashedEmailMismatch && existingHashedEmail != null) { - // If there's an existing other but it doesn't match the passed-in hashed email, log a warning - Logger.warning( - "The existing hashed email on the user ($existingHashedEmail) does not match " + - "the hashed email passed to selectPlacements ($hashedEmail). " + - "Please make sure to sync the hashed email identity to mParticle as soon as it's available. " + - "Identifying user with the provided hashed email before continuing to selectPlacements.", - ) - } - - val identityBuilder = IdentityApiRequest.withUser(user) - if (emailMismatch) { - identityBuilder.email(email) - } - if (hashedEmailMismatch && selectedIdentityType != null) { - identityBuilder.userIdentity(selectedIdentityType, hashedEmail) - } - - val identityRequest = identityBuilder.build() - val task = identityApi.identify(identityRequest) - - task.addFailureListener { result -> - Logger.error("Failed to sync email from selectPlacement to user: ${result?.errors}") - runnable.run() - } - - task.addSuccessListener { result -> - Logger.debug( - "Updated email identity based on selectPlacement's attributes: " + - result.user.userIdentities[MParticle.IdentityType.Email], - ) - runnable.run() - } - } else { - runnable.run() - } - } else { - runnable.run() - } - } -} diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitIntegrationTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitIntegrationTest.kt deleted file mode 100644 index dd8c8b096..000000000 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitIntegrationTest.kt +++ /dev/null @@ -1,235 +0,0 @@ -package com.mparticle.kits - -import android.content.Context -import android.util.SparseBooleanArray -import com.mparticle.MParticle -import com.mparticle.MParticle.IdentityType -import com.mparticle.internal.Logger -import com.mparticle.internal.MPUtility -import com.mparticle.kits.KitIntegration.AttributeListener -import com.mparticle.mock.MockMParticle -import org.junit.Assert -import org.junit.Test -import org.mockito.Mockito -import java.util.LinkedList - -class KitIntegrationTest { - @Test - @Throws(Exception::class) - fun testGetAllUserAttributesWithoutLists() { - MParticle.setInstance(MockMParticle()) - val integration: KitIntegration = - object : KitIntegration() { - override fun getName(): String? = null - - override fun onKitCreate( - settings: Map, - context: Context, - ): List? = null - - override fun setOptOut(optedOut: Boolean): List? = null - } - integration.configuration = Mockito.mock(KitConfiguration::class.java) - val attributes: MutableMap = HashMap() - attributes["key 1"] = "value 1" - val list: MutableList = LinkedList() - list.add("value 2") - list.add("value 3") - attributes["key 2"] = list - Mockito - .`when`( - MParticle - .getInstance()!! - .Identity() - .currentUser!! - .userAttributes, - ).thenReturn(attributes) - val filteredAttributes = integration.allUserAttributes - Assert.assertEquals("value 1", filteredAttributes["key 1"]) - Assert.assertEquals("value 2,value 3", filteredAttributes["key 2"]) - } - - internal inner class MockSparseBooleanArray : SparseBooleanArray() { - override fun get(key: Int): Boolean = get(key, false) - - override fun get( - key: Int, - valueIfKeyNotFound: Boolean, - ): Boolean { - Logger.verbose("SparseArray getting: $key") - return if (map.containsKey(key)) { - map[key]!! - } else { - valueIfKeyNotFound - } - } - - var map: MutableMap = HashMap() - - override fun put( - key: Int, - value: Boolean, - ) { - map[key] = value - } - - override fun clear() { - map.clear() - } - - override fun size(): Int = map.size - - override fun toString(): String = map.toString() - } - - @Test - @Throws(Exception::class) - fun testGetAllUserAttributesWithoutListsWithFilters() { - MParticle.setInstance(MockMParticle()) - val integration: KitIntegration = - object : KitIntegration() { - override fun getName(): String? = null - - override fun onKitCreate( - settings: Map, - context: Context, - ): List? = null - - override fun setOptOut(optedOut: Boolean): List? = null - } - val configuration = - Mockito.mock( - KitConfiguration::class.java, - ) - val mockArray = MockSparseBooleanArray() - mockArray.put(MPUtility.mpHash("key 4"), false) - mockArray.put(MPUtility.mpHash("key 3"), false) - Mockito.`when`(configuration.userAttributeFilters).thenReturn(mockArray) - integration.configuration = configuration - val attributes: MutableMap = HashMap() - attributes["key 1"] = "value 1" - attributes["key 4"] = "value 4" - val list: MutableList = LinkedList() - list.add("value 2") - list.add("value 3") - attributes["key 2"] = list - attributes["key 3"] = list - Mockito - .`when`( - MParticle - .getInstance()!! - .Identity() - .currentUser!! - .userAttributes, - ).thenReturn(attributes) - val filteredAttributes = integration.allUserAttributes - Assert.assertEquals("value 1", filteredAttributes["key 1"]) - Assert.assertEquals("value 2,value 3", filteredAttributes["key 2"]) - Assert.assertNull(filteredAttributes["key 3"]) - Assert.assertNull(filteredAttributes["key 4"]) - } - - @Test - @Throws(Exception::class) - fun testGetAllUserAttributesWithLists() { - MParticle.setInstance(MockMParticle()) - val integration: KitIntegration = AttributeListenerIntegration() - integration.configuration = Mockito.mock(KitConfiguration::class.java) - val attributes: MutableMap = HashMap() - attributes["key 1"] = "value 1" - val list: MutableList = LinkedList() - list.add("value 2") - list.add("value 3") - attributes["key 2"] = list - Mockito - .`when`( - MParticle - .getInstance()!! - .Identity() - .currentUser!! - .userAttributes, - ).thenReturn(attributes) - val filteredAttributes = integration.allUserAttributes - Assert.assertEquals("value 1", filteredAttributes["key 1"]) - Assert.assertEquals(list, filteredAttributes["key 2"]) - } - - @Test - @Throws(Exception::class) - fun testGetAllUserAttributesWithListsAndFilters() { - MParticle.setInstance(MockMParticle()) - val integration: KitIntegration = AttributeListenerIntegration() - val configuration = - Mockito.mock( - KitConfiguration::class.java, - ) - val mockArray = MockSparseBooleanArray() - mockArray.put(MPUtility.mpHash("key 4"), false) - mockArray.put(MPUtility.mpHash("key 3"), false) - Mockito.`when`(configuration.userAttributeFilters).thenReturn(mockArray) - integration.configuration = configuration - val attributes: MutableMap = HashMap() - attributes["key 1"] = "value 1" - attributes["key 4"] = "value 4" - val list: MutableList = LinkedList() - list.add("value 2") - list.add("value 3") - attributes["key 2"] = list - attributes["key 3"] = list - Mockito - .`when`( - MParticle - .getInstance()!! - .Identity() - .currentUser!! - .userAttributes, - ).thenReturn(attributes) - val filteredAttributes = integration.allUserAttributes - Assert.assertEquals("value 1", filteredAttributes["key 1"]) - Assert.assertEquals(list, filteredAttributes["key 2"]) - Assert.assertNull(filteredAttributes["key 3"]) - Assert.assertNull(filteredAttributes["key 4"]) - } - - private inner class AttributeListenerIntegration : - KitIntegration(), - AttributeListener { - override fun setUserAttribute( - attributeKey: String, - attributeValue: String, - ) {} - - override fun setUserAttributeList( - attributeKey: String, - attributeValueList: List, - ) {} - - override fun supportsAttributeLists(): Boolean = true - - override fun setAllUserAttributes( - userAttributes: Map, - userAttributeLists: Map>, - ) { - } - - override fun removeUserAttribute(key: String) {} - - override fun setUserIdentity( - identityType: IdentityType, - identity: String, - ) {} - - override fun removeUserIdentity(identityType: IdentityType) {} - - override fun logout(): List? = null - - override fun getName(): String? = null - - override fun onKitCreate( - settings: Map, - context: Context, - ): List? = null - - override fun setOptOut(optedOut: Boolean): List? = null - } -} diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt index 8a6407223..7e7091af9 100644 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt +++ b/android-kit-base/src/test/kotlin/com/mparticle/kits/KitManagerImplTest.kt @@ -1,16 +1,11 @@ package com.mparticle.kits -import android.content.Context -import android.graphics.Typeface import android.os.Looper import android.os.SystemClock import com.mparticle.BaseEvent import com.mparticle.MPEvent import com.mparticle.MParticle import com.mparticle.MParticleOptions -import com.mparticle.MpRoktEventCallback -import com.mparticle.RoktEvent -import com.mparticle.TypedUserAttributeListener import com.mparticle.WrapperSdk import com.mparticle.WrapperSdkVersion import com.mparticle.commerce.CommerceEvent @@ -20,36 +15,28 @@ import com.mparticle.consent.GDPRConsent import com.mparticle.identity.IdentityApi import com.mparticle.identity.MParticleUser import com.mparticle.internal.CoreCallbacks -import com.mparticle.internal.Logger import com.mparticle.internal.MPUtility import com.mparticle.internal.SideloadedKit -import com.mparticle.kits.KitIntegration.AttributeListener +import com.mparticle.kits.KitIntegration.ModifyIdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener import com.mparticle.mock.MockContext import com.mparticle.mock.MockKitConfiguration import com.mparticle.mock.MockKitManagerImpl import com.mparticle.mock.MockMParticle -import com.mparticle.rokt.PlacementOptions -import com.mparticle.rokt.RoktConfig -import com.mparticle.rokt.RoktEmbeddedView import com.mparticle.testutils.TestingUtils import junit.framework.TestCase import junit.framework.TestCase.assertEquals -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import org.junit.Assert import org.junit.Assert.assertNull -import org.junit.Assert.assertSame import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito import org.mockito.Mockito.mock import org.mockito.Mockito.never @@ -59,7 +46,6 @@ import org.mockito.Mockito.withSettings import org.powermock.api.mockito.PowerMockito import org.powermock.core.classloader.annotations.PrepareForTest import org.powermock.modules.junit4.PowerMockRunner -import java.lang.ref.WeakReference import java.util.Arrays import java.util.LinkedList import java.util.concurrent.ConcurrentHashMap @@ -92,6 +78,25 @@ class KitManagerImplTest { Assert.assertEquals(factory, manager.mKitIntegrationFactory) } + @Test + fun testActiveKitsExcludesDisabled() { + val manager: KitManagerImpl = MockKitManagerImpl() + val enabled1 = mock(KitIntegration::class.java) + val enabled2 = mock(KitIntegration::class.java) + val disabled = mock(KitIntegration::class.java) + `when`(enabled1.isDisabled()).thenReturn(false) + `when`(enabled2.isDisabled()).thenReturn(false) + `when`(disabled.isDisabled()).thenReturn(true) + manager.providers[1] = enabled1 + manager.providers[2] = disabled + manager.providers[3] = enabled2 + val active = manager.activeKits() + assertEquals(2, active.size) + assertTrue(active.contains(enabled1)) + assertTrue(active.contains(enabled2)) + Assert.assertFalse(active.contains(disabled)) + } + private fun createKitsMap( ids: List, type: Class<*> = KitIntegration::class.java, @@ -491,15 +496,15 @@ class KitManagerImplTest { val integration = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) val integration2 = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) - `when`((integration as AttributeListener).supportsAttributeLists()).thenReturn(true) - `when`((integration2 as AttributeListener).supportsAttributeLists()) + `when`((integration as UserAttributeListener).supportsAttributeLists()).thenReturn(true) + `when`((integration2 as UserAttributeListener).supportsAttributeLists()) .thenReturn(false) `when`(integration.configuration) .thenReturn(MockKitConfiguration.createKitConfiguration()) @@ -517,15 +522,15 @@ class KitManagerImplTest { attributeList.add("3") userAttributeLists["test 3"] = attributeList manager.onUserAttributesReceived(userAttributeSingles, userAttributeLists, 1L) - verify(integration as AttributeListener, Mockito.times(1)) - .setAllUserAttributes(userAttributeSingles, userAttributeLists) + verify(integration as UserAttributeListener, Mockito.times(1)) + .onSetAllUserAttributes(eq(userAttributeSingles), eq(userAttributeLists)) val userAttributesCombined: MutableMap = HashMap() userAttributesCombined["test"] = "whatever" userAttributesCombined["test 2"] = "whatever 2" userAttributesCombined["test 3"] = "1,2,3" val clearedOutList: Map> = HashMap() - verify(integration2 as AttributeListener, Mockito.times(1)) - .setAllUserAttributes(userAttributesCombined, clearedOutList) + verify(integration2 as UserAttributeListener, Mockito.times(1)) + .onSetAllUserAttributes(eq(userAttributesCombined), eq(clearedOutList)) } @Test @@ -535,15 +540,15 @@ class KitManagerImplTest { val integration = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) val integration2 = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) - `when`((integration as AttributeListener).supportsAttributeLists()).thenReturn(true) - `when`((integration2 as AttributeListener).supportsAttributeLists()) + `when`((integration as UserAttributeListener).supportsAttributeLists()).thenReturn(true) + `when`((integration2 as UserAttributeListener).supportsAttributeLists()) .thenReturn(false) `when`(integration.configuration) .thenReturn(MockKitConfiguration.createKitConfiguration()) @@ -556,10 +561,10 @@ class KitManagerImplTest { attributeList.add("2") attributeList.add("3") manager.setUserAttributeList("test key", attributeList, 1) - verify(integration as AttributeListener, Mockito.times(1)) - .setUserAttributeList("test key", attributeList) - verify(integration2 as AttributeListener, Mockito.times(1)) - .setUserAttribute("test key", "1,2,3") + verify(integration as UserAttributeListener, Mockito.times(1)) + .onSetUserAttributeList(eq("test key"), eq(attributeList)) + verify(integration2 as UserAttributeListener, Mockito.times(1)) + .onSetUserAttribute(eq("test key"), eq("1,2,3")) } @Test @@ -569,15 +574,15 @@ class KitManagerImplTest { val integration = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) val integration2 = mock( KitIntegration::class.java, - withSettings().extraInterfaces(AttributeListener::class.java), + withSettings().extraInterfaces(ModifyIdentityListener::class.java, UserAttributeListener::class.java), ) - `when`((integration as AttributeListener).supportsAttributeLists()).thenReturn(true) - `when`((integration2 as AttributeListener).supportsAttributeLists()) + `when`((integration as UserAttributeListener).supportsAttributeLists()).thenReturn(true) + `when`((integration2 as UserAttributeListener).supportsAttributeLists()) .thenReturn(false) `when`(integration.configuration) .thenReturn(MockKitConfiguration.createKitConfiguration()) @@ -890,619 +895,6 @@ class KitManagerImplTest { Assert.assertEquals(2, manager.providers.size) } - @Test - fun testRokt_non_standard_partner_user_attrs() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - {"map": "number", "value": "no"}, - {"map": "customerId", "value": "minorcatid"} - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("number", "(123) 456-9898"), - Pair("customerId", "55555"), - Pair("country", "US"), - ) - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) - Assert.assertEquals("55555", finalAttributes["minorcatid"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - - @Test - fun testSelectPlacements_shouldNotModifyAttributes_ifMappedKeysDoNotExist() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - {"map": "number", "value": "no"}, - {"map": "customerId", "value": "minorcatid"} - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("call", "(123) 456-9898"), - Pair("postal", "5-45555"), - Pair("country", "US"), - ) - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - - Assert.assertEquals("(123) 456-9898", finalAttributes["call"]) - Assert.assertEquals("5-45555", finalAttributes["postal"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - - @Test - fun testSelectPlacements_shouldNotModifyAttributes_ifMapAndValueKeysAreSame() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - {"map": "number", "value": "no"}, - {"map": "customerId", "value": "minorcatid"} - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("no", "(123) 456-9898"), - Pair("minorcatid", "5-45555"), - Pair("country", "US"), - ) - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["no"]) - Assert.assertEquals("5-45555", finalAttributes["minorcatid"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - - @Test - fun testRokt_non_standard_partner_user_attrs_When_placementAttributes_is_empty() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("number", "(123) 456-9898"), - Pair("customerId", "55555"), - Pair("country", "US"), - ) - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) - Assert.assertEquals("55555", finalAttributes["customerId"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - - @Test - fun testRokt_SandboxMode_When_Default_Environment() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - `when`(sideloadedKit.configuration).thenReturn(mockedKitConfig) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("number", "(123) 456-9898"), - Pair("customerId", "55555"), - Pair("country", "US"), - ) - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) - Assert.assertEquals("55555", finalAttributes["customerId"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - - @Test - fun testRokt_selectPlacements_with_PlacementOptions() { - val mockUser = mock(MParticleUser::class.java) - stubUserAttributesCallback(mockUser) - `when`(mockIdentity!!.currentUser).thenReturn(mockUser) - - val manager: KitManagerImpl = MockKitManagerImpl() - val roktListener = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(roktListener.isDisabled).thenReturn(false) - manager.providers = - ConcurrentHashMap().apply { - put(1, roktListener) - } - - val attributes = hashMapOf() - val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null, placementOptions) - - val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) - verify(roktListener as KitIntegration.RoktListener).selectPlacements( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - optionsCaptor.capture(), - ) - assertSame(placementOptions, optionsCaptor.value) - } - - @Test - fun testRokt_selectPlacements_without_PlacementOptions() { - val mockUser = mock(MParticleUser::class.java) - stubUserAttributesCallback(mockUser) - `when`(mockIdentity!!.currentUser).thenReturn(mockUser) - - val manager: KitManagerImpl = MockKitManagerImpl() - val roktListener = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(roktListener.isDisabled).thenReturn(false) - manager.providers = - ConcurrentHashMap().apply { - put(1, roktListener) - } - - val attributes = hashMapOf() - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - - val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) - verify(roktListener as KitIntegration.RoktListener).selectPlacements( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - optionsCaptor.capture(), - ) - assertNull(optionsCaptor.value) - } - - @Test - fun testRokt_SandboxMode_When_Environment_IS_Development() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - PowerMockito.mockStatic(MPUtility::class.java) - `when`(MPUtility.isDevEnv()).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("number", "(123) 456-9898"), - Pair("customerId", "55555"), - Pair("country", "US"), - ) - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) - Assert.assertEquals("55555", finalAttributes["customerId"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("true", finalAttributes["sandbox"]) - } - - @Test - fun testRokt_SandboxMode_When_SandBox_is_Pass_In_Attributes_And_Environment_Is_DEV() { - val sideloadedKit = mock(MPSideloadedKit::class.java) - val kitId = 6000000 - - val configJSONObj = - JSONObject().apply { - put("id", kitId) - } - val mockedKitConfig = KitConfiguration.createKitConfiguration(configJSONObj) - - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(mockedKitConfig, settingsMap) - - val mockedProvider = MockProvider(mockedKitConfig) - - val options = - MParticleOptions - .builder(MockContext()) - .sideloadedKits(mutableListOf(sideloadedKit) as List) - .build() - val manager: KitManagerImpl = MockKitManagerImpl(options) - val factory = mock(KitIntegrationFactory::class.java) - manager.setKitFactory(factory) - - `when`(factory.isSupported(Mockito.anyInt())).thenReturn(true) - PowerMockito.mockStatic(MPUtility::class.java) - `when`(MPUtility.isDevEnv()).thenReturn(true) - val supportedKit = mutableSetOf(kitId) - `when`(manager.supportedKits).thenReturn(supportedKit) - `when`(sideloadedKit.isDisabled).thenReturn(false) - `when`( - factory.createInstance( - any( - KitManagerImpl::class.java, - ), - any(KitConfiguration::class.java), - ), - ).thenReturn(sideloadedKit) - manager.providers = - ConcurrentHashMap().apply { - put(42, mockedProvider) - } - - val attributes = - hashMapOf( - Pair("test", "Test"), - Pair("lastname", "Test1"), - Pair("number", "(123) 456-9898"), - Pair("customerId", "55555"), - Pair("country", "US"), - Pair("sandbox", "false"), - ) - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - roktApi!!.selectPlacements("Test", attributes, null, null, null, null) - val finalAttributes = mockedProvider.lastAttributes - Assert.assertNotNull(finalAttributes) - finalAttributes!! - Assert.assertEquals(6, finalAttributes.size) - Assert.assertEquals("(123) 456-9898", finalAttributes["number"]) - Assert.assertEquals("55555", finalAttributes["customerId"]) - Assert.assertEquals("Test1", finalAttributes["lastname"]) - Assert.assertEquals("Test", finalAttributes["test"]) - Assert.assertEquals("US", finalAttributes["country"]) - Assert.assertEquals("false", finalAttributes["sandbox"]) - } - @Test fun testSetWrapperSdkVersion() { val manager: KitManagerImpl = MockKitManagerImpl() @@ -1540,316 +932,6 @@ class KitManagerImplTest { .setWrapperSdkVersion(wrapperSdkVersion) } - @Test - fun testEvents_noProviders_returnsEmptyFlow() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val roktApi = manager.getRoktKitApi() - assertNull(roktApi) - val result = roktApi?.events("test-identifier") ?: flowOf() - - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - @Test - fun testEvents_providersExistButNotRoktListeners_returnsEmptyFlow() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val nonRoktProvider = mock(KitIntegration::class.java) - `when`(nonRoktProvider.isDisabled).thenReturn(false) - `when`(nonRoktProvider.getName()).thenReturn("NonRoktProvider") - - manager.providers = - ConcurrentHashMap().apply { - put(1, nonRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - assertNull(roktApi) - val result = roktApi?.events("test-identifier") ?: flowOf() - - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - @Test - fun testEvents_roktListenerDisabled_returnsEmptyFlow() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val disabledRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(disabledRoktProvider.isDisabled).thenReturn(true) - `when`(disabledRoktProvider.getName()).thenReturn("DisabledRoktProvider") - - manager.providers = - ConcurrentHashMap().apply { - put(1, disabledRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - assertNull(roktApi) - val result = roktApi?.events("test-identifier") ?: flowOf() - - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - @Test - fun testEvents_roktListenerEnabled_delegatesToProvider() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val enabledRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(enabledRoktProvider.isDisabled).thenReturn(false) - `when`(enabledRoktProvider.getName()).thenReturn("EnabledRoktProvider") - - val expectedFlow: Flow = flowOf() - val testIdentifier = "test-identifier" - `when`((enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier)) - .thenReturn(expectedFlow) - - manager.providers = - ConcurrentHashMap().apply { - put(1, enabledRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - val result = roktApi!!.events(testIdentifier) - - verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) - assertEquals(expectedFlow, result) - } - - @Test - fun testEvents_multipleProviders_usesFirstEnabledRoktListener() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val nonRoktProvider = mock(KitIntegration::class.java) - `when`(nonRoktProvider.isDisabled).thenReturn(false) - `when`(nonRoktProvider.getName()).thenReturn("NonRoktProvider") - - val disabledRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(disabledRoktProvider.isDisabled).thenReturn(true) - `when`(disabledRoktProvider.getName()).thenReturn("DisabledRoktProvider") - - val enabledRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(enabledRoktProvider.isDisabled).thenReturn(false) - `when`(enabledRoktProvider.getName()).thenReturn("EnabledRoktProvider") - - val secondEnabledRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(secondEnabledRoktProvider.isDisabled).thenReturn(false) - `when`(secondEnabledRoktProvider.getName()).thenReturn("SecondEnabledRoktProvider") - - val expectedFlow: Flow = flowOf() - val testIdentifier = "test-identifier" - `when`((enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier)) - .thenReturn(expectedFlow) - `when`((secondEnabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier)) - .thenReturn(flowOf()) - - manager.providers = - ConcurrentHashMap().apply { - put(1, nonRoktProvider) - put(2, disabledRoktProvider) - put(3, enabledRoktProvider) - put(4, secondEnabledRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - val result = roktApi!!.events(testIdentifier) - - verify(enabledRoktProvider as KitIntegration.RoktListener).events(testIdentifier) - verify(secondEnabledRoktProvider as KitIntegration.RoktListener, never()).events(any()) - assertEquals(expectedFlow, result) - } - - @Test - fun testEvents_providerThrowsException_returnsEmptyFlow() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val exceptionRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(exceptionRoktProvider.isDisabled).thenReturn(false) - `when`(exceptionRoktProvider.getName()).thenReturn("ExceptionRoktProvider") - `when`((exceptionRoktProvider as KitIntegration.RoktListener).events(any())) - .thenThrow(RuntimeException("Test exception")) - - manager.providers = - ConcurrentHashMap().apply { - put(1, exceptionRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - val result = roktApi!!.events("test-identifier") - - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - @Test - fun testEvents_providerThrowsException_returnsEmptyFlowWithoutFallback() { - val manager: KitManagerImpl = MockKitManagerImpl() - - val exceptionRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(exceptionRoktProvider.isDisabled).thenReturn(false) - `when`(exceptionRoktProvider.getName()).thenReturn("ExceptionRoktProvider") - `when`((exceptionRoktProvider as KitIntegration.RoktListener).events(any())) - .thenThrow(RuntimeException("Test exception")) - - val workingRoktProvider = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(workingRoktProvider.isDisabled).thenReturn(false) - `when`(workingRoktProvider.getName()).thenReturn("WorkingRoktProvider") - - val expectedFlow: Flow = flowOf() - val testIdentifier = "test-identifier" - `when`((workingRoktProvider as KitIntegration.RoktListener).events(testIdentifier)) - .thenReturn(expectedFlow) - - manager.providers = - ConcurrentHashMap().apply { - put(1, exceptionRoktProvider) - put(2, workingRoktProvider) - } - - val roktApi = manager.getRoktKitApi() - Assert.assertNotNull(roktApi) - val result = roktApi!!.events(testIdentifier) - - verify(workingRoktProvider as KitIntegration.RoktListener, never()).events(any()) - runTest { - val elements = result.toList() - assertTrue(elements.isEmpty()) - } - } - - private fun stubUserAttributesCallback(user: MParticleUser) { - `when`(user.getUserAttributes(any())).thenAnswer { invocation -> - val listener = invocation.arguments[0] as TypedUserAttributeListener - listener.onUserAttributesReceived(emptyMap(), emptyMap(), 0L) - null - } - } - - internal inner class MockProvider( - val config: KitConfiguration, - ) : KitIntegration(), - KitIntegration.RoktListener { - var lastAttributes: Map? = null - var lastOptions: PlacementOptions? = null - var lastUser: FilteredMParticleUser? = null - - override fun isDisabled(): Boolean = false - - override fun getName(): String = "FakeProvider" - - override fun onKitCreate( - settings: MutableMap?, - context: Context?, - ): MutableList { - TODO("Not yet implemented") - } - - override fun setOptOut(optedOut: Boolean): MutableList { - TODO("Not yet implemented") - } - - override fun getConfiguration(): KitConfiguration = config - - override fun selectPlacements( - viewName: String, - attributes: MutableMap, - mpRoktEventCallback: MpRoktEventCallback?, - placeHolders: MutableMap>?, - fontTypefaces: MutableMap>?, - user: FilteredMParticleUser?, - config: RoktConfig?, - options: PlacementOptions?, - ) { - lastAttributes = attributes.toMap() - lastOptions = options - lastUser = user - Logger.info("selectPlacements with $attributes and options $options") - } - - override fun events(identifier: String): Flow { - Logger.info("events called with identfier: $identifier") - return flowOf() - } - - override fun enrichAttributes( - attributes: MutableMap, - user: FilteredMParticleUser?, - ) { - Logger.info("callRoktComposable with $attributes") - } - - override fun setWrapperSdkVersion(wrapperSdkVersion: WrapperSdkVersion) { - Logger.info("setWrapperSdkVersion with $wrapperSdkVersion") - } - - override fun purchaseFinalized( - placementId: String, - catalogItemId: String, - status: Boolean, - ) { - Logger.info("purchaseFinalized with placementId: $placementId catalogItemId : $catalogItemId status : $status ") - } - - override fun close() { - Logger.info("close called") - } - - override fun setSessionId(sessionId: String) { - Logger.info("setSessionId called with $sessionId") - } - - override fun getSessionId(): String? { - Logger.info("getSessionId called") - return null - } - } - internal inner class KitManagerEventCounter : MockKitManagerImpl() { var logBaseEventCalled = 0 var logCommerceEventCalled = 0 diff --git a/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt b/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt deleted file mode 100644 index 2f27fa440..000000000 --- a/android-kit-base/src/test/kotlin/com/mparticle/kits/RoktKitApiImplTest.kt +++ /dev/null @@ -1,224 +0,0 @@ -package com.mparticle.kits - -import com.mparticle.MParticle -import com.mparticle.identity.IdentityApi -import com.mparticle.identity.MParticleUser -import com.mparticle.internal.MPUtility -import com.mparticle.mock.MockMParticle -import com.mparticle.rokt.PlacementOptions -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.test.runTest -import org.json.JSONObject -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any -import org.mockito.Mockito.mock -import org.mockito.Mockito.never -import org.mockito.Mockito.verify -import org.mockito.Mockito.`when` -import org.mockito.Mockito.withSettings - -class RoktKitApiImplTest { - @Before - fun setUp() { - val identityApi = mock(IdentityApi::class.java) - val instance = MockMParticle() - instance.setIdentityApi(identityApi) - MParticle.setInstance(instance) - } - - @Test - fun testSelectPlacements_mapsAttributesAndAddsSandbox() { - val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - {"map": "number", "value": "no"}, - {"map": "customerId", "value": "minorcatid"} - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(kitConfig, settingsMap) - - val kitIntegration = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(kitIntegration.configuration).thenReturn(kitConfig) - val roktListener = kitIntegration as KitIntegration.RoktListener - val roktApi = RoktKitApiImpl(roktListener, kitIntegration) - - val attributes = - hashMapOf( - "number" to "(123) 456-9898", - "customerId" to "55555", - "country" to "US", - ) - - roktApi.selectPlacements("Test", attributes, null, null, null, null, null) - - @Suppress("UNCHECKED_CAST") - val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> - verify(roktListener).selectPlacements( - any(), - attributesCaptor.capture(), - any(), - any(), - any(), - any(), - any(), - any(), - ) - val captured = attributesCaptor.value - assertEquals("(123) 456-9898", captured["no"]) - assertEquals("55555", captured["minorcatid"]) - assertEquals("US", captured["country"]) - assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) - } - - @Test - fun testSelectPlacements_waitsForUserAttributesBeforeDelegating() { - val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) - val kitIntegration = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(kitIntegration.configuration).thenReturn(kitConfig) - val roktListener = kitIntegration as KitIntegration.RoktListener - val roktApi = RoktKitApiImpl(roktListener, kitIntegration) - - val identityApi = mock(IdentityApi::class.java) - val user = mock(MParticleUser::class.java) - `when`(user.id).thenReturn(12345L) - `when`(identityApi.currentUser).thenReturn(user) - val instance = MockMParticle() - instance.setIdentityApi(identityApi) - MParticle.setInstance(instance) - - var capturedListener: com.mparticle.TypedUserAttributeListener? = null - `when`(user.getUserAttributes(any())).thenAnswer { invocation -> - capturedListener = invocation.arguments[0] as com.mparticle.TypedUserAttributeListener - null - } - - val attributes = mapOf("country" to "US") - roktApi.selectPlacements("Test", attributes, null, null, null, null, null) - - verify(user).setUserAttributes(any()) - verify(roktListener, never()).selectPlacements( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - ) - - capturedListener!!.onUserAttributesReceived(emptyMap(), emptyMap(), 12345L) - - verify(roktListener).selectPlacements( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - any(), - ) - } - - @Test - fun testSelectPlacements_passesPlacementOptions() { - val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) - val kitIntegration = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(kitIntegration.configuration).thenReturn(kitConfig) - val roktListener = kitIntegration as KitIntegration.RoktListener - val roktApi = RoktKitApiImpl(roktListener, kitIntegration) - - val placementOptions = PlacementOptions(jointSdkSelectPlacements = 123L) - - roktApi.selectPlacements("Test", emptyMap(), null, null, null, null, placementOptions) - - val optionsCaptor = ArgumentCaptor.forClass(PlacementOptions::class.java) - verify(roktListener).selectPlacements( - any(), - any(), - any(), - any(), - any(), - any(), - any(), - optionsCaptor.capture(), - ) - assertEquals(placementOptions, optionsCaptor.value) - } - - @Test - fun testEvents_returnsEmptyFlowWhenProviderThrows() = runTest { - val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) - val kitIntegration = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(kitIntegration.configuration).thenReturn(kitConfig) - val roktListener = kitIntegration as KitIntegration.RoktListener - `when`(roktListener.events(any())).thenThrow(RuntimeException("Test exception")) - val roktApi = RoktKitApiImpl(roktListener, kitIntegration) - - val result = roktApi.events("test-identifier") - - assertTrue(result.toList().isEmpty()) - } - - @Test - fun testPrepareAttributesAsync_delegatesToEnrichAttributes() { - val kitConfig = KitConfiguration.createKitConfiguration(JSONObject().put("id", 42)) - val settingsMap = - hashMapOf( - "placementAttributesMapping" to - """ - [ - {"map": "number", "value": "no"} - ] - """.trimIndent(), - ) - val field = KitConfiguration::class.java.getDeclaredField("settings") - field.isAccessible = true - field.set(kitConfig, settingsMap) - - val kitIntegration = - mock( - KitIntegration::class.java, - withSettings().extraInterfaces(KitIntegration.RoktListener::class.java), - ) - `when`(kitIntegration.configuration).thenReturn(kitConfig) - val roktListener = kitIntegration as KitIntegration.RoktListener - val roktApi = RoktKitApiImpl(roktListener, kitIntegration) - - roktApi.prepareAttributesAsync(mapOf("number" to "(123) 456-9898")) - - @Suppress("UNCHECKED_CAST") - val attributesCaptor = ArgumentCaptor.forClass(Map::class.java) as ArgumentCaptor> - verify(roktListener).enrichAttributes(attributesCaptor.capture(), any()) - val captured = attributesCaptor.value - assertEquals("(123) 456-9898", captured["no"]) - assertEquals(MPUtility.isDevEnv().toString(), captured["sandbox"]) - } -} diff --git a/build.gradle b/build.gradle index f4722977d..d6bab31e2 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,7 @@ buildscript { - ext.kotlin_version = '1.9.0' - ext.gradle_version = '8.1.4' + ext.kotlin_version = '2.1.20' + ext.coroutines_version = '1.9.0' + ext.gradle_version = '8.3.2' repositories { mavenCentral() @@ -8,7 +9,7 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:$gradle_version" - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.20" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } @@ -63,11 +64,6 @@ subprojects { allprojects { group = 'com.mparticle' - version = '5.80.0-SNAPSHOT' - if (project.hasProperty('isRelease') && project.isRelease) { - version = version.toString().replace("-SNAPSHOT", "") - } - repositories { mavenLocal() google() diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 000000000..b2502fb71 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,36 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "2.1.20" + `kotlin-dsl` +} + +repositories { + google() + mavenCentral() + gradlePluginPortal() +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions { jvmTarget = JavaVersion.VERSION_17.toString() } +} + +dependencies { + implementation("com.android.tools.build:gradle:8.3.2") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.1.20") + implementation("com.vanniktech:gradle-maven-publish-plugin:0.31.0") +} + +gradlePlugin { + plugins { + register("androidLibraryPublish") { + id = "mparticle.android.library.publish" + implementationClass = "AndroidLibraryMavenCentralPublishPlugin" + } + } +} diff --git a/buildSrc/src/main/kotlin/AndroidLibraryMavenCentralPublishPlugin.kt b/buildSrc/src/main/kotlin/AndroidLibraryMavenCentralPublishPlugin.kt new file mode 100644 index 000000000..4f32b46b6 --- /dev/null +++ b/buildSrc/src/main/kotlin/AndroidLibraryMavenCentralPublishPlugin.kt @@ -0,0 +1,14 @@ +import com.mparticle.configureMavenPublishing +import com.mparticle.publish.MParticleMavenPublishExtension +import org.gradle.api.Plugin +import org.gradle.api.Project + +class AndroidLibraryMavenCentralPublishPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.vanniktech.maven.publish") + val publishExtension = extensions.create("mparticleMavenPublish", MParticleMavenPublishExtension::class.java) + configureMavenPublishing(publishExtension) + } + } +} diff --git a/buildSrc/src/main/kotlin/com/mparticle/MavenCentralPublish.kt b/buildSrc/src/main/kotlin/com/mparticle/MavenCentralPublish.kt new file mode 100644 index 000000000..a9a15dc2e --- /dev/null +++ b/buildSrc/src/main/kotlin/com/mparticle/MavenCentralPublish.kt @@ -0,0 +1,110 @@ +package com.mparticle + +import com.mparticle.publish.MParticleMavenPublishExtension +import com.vanniktech.maven.publish.AndroidSingleVariantLibrary +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.publish.maven.tasks.PublishToMavenLocal +import org.gradle.jvm.tasks.Jar + +fun Project.configureMavenPublishing(mparticleMavenPublish: MParticleMavenPublishExtension) { + val versionFromProperty = project.findProperty("VERSION")?.toString() + .takeIf { !it.isNullOrBlank() } ?: "0.0.0" + val shouldSign = !project.findProperty("signingInMemoryKey")?.toString().isNullOrBlank() + // Capture before entering configure block where `this` changes to MavenPublishBaseExtension + val isAndroidLibrary = pluginManager.hasPlugin("com.android.library") + + extensions.configure("mavenPublishing") { + // Dokka (bundled with AGP 8.3.2) uses ASM8, which cannot handle PermittedSubclasses + // attributes present in sealed classes compiled with JVM 17 (e.g. RoktEvent in + // android-core). Prevent AGP's withJavadocJar() from registering the failing + // javaDocReleaseGeneration task; we attach a stub empty javadoc jar in afterEvaluate. + if (isAndroidLibrary) { + configure(AndroidSingleVariantLibrary(sourcesJar = true, publishJavadocJar = false)) + } + + val publicationName = "maven" + + afterEvaluate { + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) + coordinates( + groupId = mparticleMavenPublish.groupId.getOrElse("com.mparticle"), + artifactId = mparticleMavenPublish.artifactId.getOrElse(project.name), + version = versionFromProperty, + ) + if (shouldSign) { + signAllPublications() + } else { + logger.lifecycle("Skipping signAllPublications for ${project.name} (no signingInMemoryKey)") + } + val mparticleRepoUrl = "https://github.com/mparticle/mparticle-android-sdk" + pom { + name.set(mparticleMavenPublish.description.getOrElse(project.name)) + description.set(mparticleMavenPublish.description.getOrElse(project.name)) + url.set(mparticleMavenPublish.pomUrl.getOrElse(mparticleRepoUrl)) + licenses { + license { + name.set( + mparticleMavenPublish.licenseName + .getOrElse("The Apache Software License, Version 2.0"), + ) + url.set( + mparticleMavenPublish.licenseUrl + .getOrElse("https://www.apache.org/licenses/LICENSE-2.0.txt"), + ) + } + } + developers { + developer { + id.set("mParticle") + name.set("mParticle Inc.") + email.set("developers@mparticle.com") + } + } + scm { + url.set("https://github.com/mparticle/mparticle-android-sdk") + connection.set("scm:git:https://github.com/mparticle/mparticle-android-sdk") + developerConnection.set("scm:git:git@github.com:mparticle/mparticle-android-sdk.git") + } + } + + // Attach a stub empty javadoc jar to satisfy Maven Central requirements. + // The javaDocReleaseGeneration Dokka task was intentionally bypassed above. + if (isAndroidLibrary) { + val emptyJavadocJar = tasks.register("emptyJavadocJar", Jar::class.java) + emptyJavadocJar.configure { archiveClassifier.set("javadoc") } + project.extensions.getByType(PublishingExtension::class.java) + .publications + .withType(MavenPublication::class.java) + .matching { it.name == publicationName } + .configureEach { artifact(emptyJavadocJar) } + } + } + + val capitalizedPublicationName = publicationName.capitalizeForTaskName() + val validateTaskName = + "validatePomFor${capitalizedPublicationName}Publication" + + tasks.register(validateTaskName, ValidatePomTask::class.java) { + description = "Validates the generated POM file for the '$publicationName' publication." + group = "verification" + pomFile.set(project.layout.buildDirectory.file("publications/$publicationName/pom-default.xml")) + dependsOn("generatePomFileFor${capitalizedPublicationName}Publication") + } + + tasks.withType(PublishToMavenLocal::class.java).configureEach { + if (name.contains(publicationName, ignoreCase = true)) { + dependsOn(validateTaskName) + } + } + } +} + +private fun String.capitalizeForTaskName(): String = if (isEmpty()) { + this +} else { + substring(0, 1).uppercase() + substring(1) +} diff --git a/buildSrc/src/main/kotlin/com/mparticle/ValidatePomTask.kt b/buildSrc/src/main/kotlin/com/mparticle/ValidatePomTask.kt new file mode 100644 index 000000000..8bf6e0ac7 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/mparticle/ValidatePomTask.kt @@ -0,0 +1,128 @@ +package com.mparticle + +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.* +import org.w3c.dom.Document +import org.w3c.dom.Element +import org.w3c.dom.NodeList +import javax.xml.parsers.DocumentBuilderFactory + +abstract class ValidatePomTask : DefaultTask() { + + @get:InputFile + @get:PathSensitive(PathSensitivity.NONE) + abstract val pomFile: RegularFileProperty + + @TaskAction + fun execute() { + val pom = pomFile.get().asFile + if (!pom.exists()) { + throw GradleException("POM file not found: ${pom.absolutePath}") + } + + val factory = DocumentBuilderFactory.newInstance() + val builder = factory.newDocumentBuilder() + val doc: Document = builder.parse(pom) + doc.documentElement.normalize() + + val errors = mutableListOf() + val projectElement = doc.documentElement // Root element + + // Helper function to get text content of the first element by tag name + fun getFirstElementText(parent: Element, tagName: String): String? { + val nodeList = parent.getElementsByTagName(tagName) + return if (nodeList.length > 0) nodeList.item(0).textContent?.trim() else null + } + + // Helper function to check if text content is null or empty + fun checkNotEmpty(parent: Element, tagName: String, errorMessage: String) { + if (getFirstElementText(parent, tagName).isNullOrEmpty()) { + errors.add(errorMessage) + } + } + + // --- Basic Structure Checks --- + checkNotEmpty(projectElement, "groupId", "Missing or empty ") + checkNotEmpty(projectElement, "artifactId", "Missing or empty ") + checkNotEmpty(projectElement, "version", "Missing or empty ") + checkNotEmpty(projectElement, "name", "Missing or empty ") + checkNotEmpty(projectElement, "description", "Missing or empty ") + checkNotEmpty(projectElement, "url", "Missing or empty ") + + // --- License Check --- + val licensesNode = projectElement.getElementsByTagName("licenses").item(0) as? Element + val licenseNodes = licensesNode?.getElementsByTagName("license") + if (licenseNodes == null || licenseNodes.length == 0) { + errors.add("Missing section or entry") + } else { + for (i in 0 until licenseNodes.length) { + val lic = licenseNodes.item(i) as Element + checkNotEmpty(lic, "name", "Missing in license") + checkNotEmpty(lic, "url", "Missing in license") + } + } + + // --- Developer Check --- + val developersNode = projectElement.getElementsByTagName("developers").item(0) as? Element + val developerNodes = developersNode?.getElementsByTagName("developer") + if (developerNodes == null || developerNodes.length == 0) { + errors.add("Missing section or entry") + } else { + for (i in 0 until developerNodes.length) { + val dev = developerNodes.item(i) as Element + val devId = getFirstElementText(dev, "id") + val devName = getFirstElementText(dev, "name") + val devEmail = getFirstElementText(dev, "email") + if (devId.isNullOrEmpty() && devName.isNullOrEmpty() && devEmail.isNullOrEmpty()) { + errors.add("Developer entry needs at least , , or ") + } + } + } + + // --- SCM Check --- + val scmNode = projectElement.getElementsByTagName("scm").item(0) as? Element + if (scmNode == null) { + errors.add("Missing section") + } else { + checkNotEmpty(scmNode, "connection", "Missing ") + checkNotEmpty(scmNode, "developerConnection", "Missing ") + checkNotEmpty(scmNode, "url", "Missing ") + } + + // --- Dependency Version Check --- + val dependenciesNode = projectElement.getElementsByTagName("dependencies").item(0) as? Element + val dependencyNodes: NodeList? = dependenciesNode?.getElementsByTagName("dependency") + if (dependencyNodes != null) { + for (i in 0 until dependencyNodes.length) { + val dep = dependencyNodes.item(i) as Element + val groupId = getFirstElementText(dep, "groupId") + val artifactId = getFirstElementText(dep, "artifactId") + val version = getFirstElementText(dep, "version") + val scope = getFirstElementText(dep, "scope") + val depId = "${groupId ?: "MISSING_GROUP"}:${artifactId ?: "MISSING_ARTIFACT"}" + + if (groupId.isNullOrEmpty()) { + errors.add("Dependency entry is missing .") + } + if (artifactId.isNullOrEmpty()) { + errors.add("Dependency entry is missing .") + } + + // Check specifically for missing tag within a + // Ignore dependencies with import (BOMs) + if (version.isNullOrEmpty() && scope != "import") { + errors.add("Dependency $depId is missing required tag.") + } + } + } + + // --- Report Errors --- + if (errors.isNotEmpty()) { + throw GradleException("POM validation failed for ${pom.name}:\n - ${errors.joinToString("\n - ")}") + } else { + logger.lifecycle("POM validation successful for ${pom.name}") + } + } +} diff --git a/buildSrc/src/main/kotlin/com/mparticle/publish/MParticleMavenPublishExtension.kt b/buildSrc/src/main/kotlin/com/mparticle/publish/MParticleMavenPublishExtension.kt new file mode 100644 index 000000000..3a82f9900 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/mparticle/publish/MParticleMavenPublishExtension.kt @@ -0,0 +1,17 @@ +package com.mparticle.publish + +import org.gradle.api.provider.Property + +interface MParticleMavenPublishExtension { + val groupId: Property + val artifactId: Property + val description: Property + + // Optional POM branding overrides. When unset, the convention plugin falls back to the + // mParticle defaults (Apache 2.0 license, mParticle project URL). Used by non-mParticle + // artifacts published from this repo (e.g. com.rokt:rokt-sdk-plus) so their POM carries + // the correct project URL and license. + val pomUrl: Property + val licenseName: Property + val licenseUrl: Property +} diff --git a/gradle.properties b/gradle.properties index 2e22ec6d1..444938580 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,10 @@ org.gradle.jvmargs=-Xmx2560m android.defaults.buildfeatures.buildconfig=true systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true JAVA_VERSION=17 + +# Rokt SDK artifact versions. These ride their own release line (independent of the mParticle +# SDK VERSION) and are the single source of truth shared by the Rokt kit (com.rokt:roktsdk) and +# the rokt-sdk-plus umbrella (com.rokt:payment-extension). Bump together when adopting a new +# Rokt SDK release. +roktSdkVersion=6.0.1 +roktPaymentExtensionVersion=6.0.1 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index bdc9a83b1..3499ded5c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/kit-plugin/build.gradle b/kit-plugin/build.gradle index 1dd5660b1..9891f1eff 100644 --- a/kit-plugin/build.gradle +++ b/kit-plugin/build.gradle @@ -1,11 +1,11 @@ apply plugin: 'groovy' +apply plugin: 'mparticle.android.library.publish' -ext { - kitDescription = 'mParticle Gradle Plugin for creating mParticle kits' +mparticleMavenPublish { + artifactId.set('android-kit-plugin') + description.set('mParticle Gradle Plugin for creating mParticle kits') } -apply from: '../scripts/maven.gradle' - dependencies { implementation gradleApi() implementation localGroovy() @@ -26,12 +26,4 @@ tasks.withType(Test) { jvmArgs('--add-opens=java.base/java.lang=ALL-UNNAMED') } -task generateJavadocsJar(type: Jar, dependsOn: groovydoc) { - archiveClassifier.set("javadoc") - from groovydoc.destinationDir -} -task generateSourcesJar(type: Jar) { - archiveClassifier.set("sources") - from sourceSets.main.java.srcDirs -} diff --git a/kit-plugin/src/main/groovy/com/mparticle/kits/KitPlugin.groovy b/kit-plugin/src/main/groovy/com/mparticle/kits/KitPlugin.groovy index ac016fb20..9c5cf7c53 100644 --- a/kit-plugin/src/main/groovy/com/mparticle/kits/KitPlugin.groovy +++ b/kit-plugin/src/main/groovy/com/mparticle/kits/KitPlugin.groovy @@ -3,9 +3,7 @@ package com.mparticle.kits import com.android.build.gradle.LibraryExtension import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.publish.PublishingExtension -import org.gradle.api.publish.maven.MavenPublication -import org.gradle.plugins.signing.SigningExtension +import org.gradle.api.artifacts.dsl.RepositoryHandler class KitPlugin implements Plugin { void apply(Project target) { @@ -13,14 +11,15 @@ class KitPlugin implements Plugin { //formerly in kit-common.gradle target.apply(plugin: 'com.android.library') target.group = 'com.mparticle' - target.buildscript.repositories.add(target.repositories.mavenLocal()) - target.buildscript.repositories.add(target.repositories.google()) - target.buildscript.repositories.add(target.repositories.mavenCentral()) - target.repositories.add(target.repositories.mavenLocal()) - target.repositories.add(target.repositories.google()) - target.repositories.add(target.repositories.mavenCentral()) + boolean mparticleFromMavenLocalOnly = resolveMparticleFromMavenLocalOnly(target) + configureRepositories(target.buildscript.repositories, mparticleFromMavenLocalOnly) + configureRepositories(target.repositories, mparticleFromMavenLocalOnly) target.configurations.create('deployerJars') - target.dependencies.add('api', 'com.mparticle:android-kit-base:' + target.version) + // VERSION from CI is often set only on the root project (e.g. ORG_GRADLE_PROJECT_VERSION); kit + // subprojects do not always inherit it, which would fall back to '+' and unstable resolution. + def kitBaseVersion = + target.findProperty('VERSION') ?: target.rootProject.findProperty('VERSION') ?: '+' + target.dependencies.add('api', 'com.mparticle:android-kit-base:' + kitBaseVersion) target.dependencies.add('testImplementation', 'junit:junit:4.13.2') target.dependencies.add('testImplementation', 'org.mockito:mockito-core:1.10.19') target.dependencies.add('testImplementation', 'androidx.annotation:annotation:[1.0.0,)') @@ -44,121 +43,62 @@ class KitPlugin implements Plugin { jvmArgs += ['--add-opens', 'java.base/java.util.concurrent=ALL-UNNAMED'] } - //formerly in maven.gradle - target.apply(plugin: 'maven-publish') - target.apply(plugin: 'signing') - - target.afterEvaluate { - PublishingExtension publishing = target.extensions.findByName('publishing') - publishing.publications.create("release", MavenPublication.class) { - groupId = "com.mparticle" - artifactId = target.name - version = target.version - if (target.plugins.findPlugin("com.android.library")) { - from target.components.release - } else { - from target.components.java - } + // mparticle.android.library.publish is only available when building inside the + // monorepo (buildSrc on classpath). Standalone kit builds and test fixtures skip + // Maven publishing configuration — kits will be migrated to the monorepo. + try { + target.apply(plugin: 'mparticle.android.library.publish') + def publishExt = target.extensions.getByName('mparticleMavenPublish') + publishExt.artifactId.convention(target.providers.provider { target.name }) + publishExt.description.convention(target.providers.provider { + String desc = target.extensions.findByName('mparticle')?.kitDescription + desc ?: (target.name + ' for the mParticle SDK') + }) + } catch (org.gradle.api.plugins.UnknownPluginException ignored) { + // no-op: publish plugin unavailable outside monorepo + } + } - pom { - artifactId = target.name - packaging = 'aar' - name = target.name - if (target.mparticle.kitDescription == null) { - description = target.name + ' for the mParticle SDK' - } else { - description = target.mparticle.kitDescription - } - url = 'https://github.com/mparticle/mparticle-sdk-android' - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'https://www.apache.org/license/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'mParticle' - name = 'mParticle Inc.' - email = 'developers@mparticle.com' - } - } - scm { - url = 'https://github.com/mparticle/mparticle-android-sdk' - connection = 'scm:git:https://github.com/mparticle/mparticle-android-sdk' - developerConnection = 'scm:git:git@github.com:mparticle/mparticle-android-sdk.git' - } - } - } - publishing.publications.register("debug", MavenPublication.class) { - groupId = "com.mparticle" - artifactId = target.name - version = target.version - if (target.plugins.findPlugin("com.android.library")) { - from target.components.debug - } else { - from target.components.java - } + /** + * When true, {@code com.mparticle} artifacts (e.g. android-kit-base, android-kit-plugin) resolve + * only from {@code mavenLocal()}; Maven Central / Google are not queried for that group. + * Use after {@code publishMavenPublicationToMavenLocal} so CI and monorepo kit builds use the + * freshly published local artifacts. Enable with {@code -Pmparticle.kit.mparticleFromMavenLocalOnly=true} + * or env {@code MPARTICLE_KIT_FROM_MAVEN_LOCAL_ONLY=true}. + */ + private static boolean resolveMparticleFromMavenLocalOnly(Project target) { + String prop = target.findProperty('mparticle.kit.mparticleFromMavenLocalOnly')?.toString() + if (prop != null && Boolean.parseBoolean(prop)) { + return true + } + String env = System.getenv('MPARTICLE_KIT_FROM_MAVEN_LOCAL_ONLY') + return env != null && 'true'.equalsIgnoreCase(env) + } - pom { - artifactId = target.name - packaging = 'aar' - name = target.name - if (target.mparticle.kitDescription == null) { - description = target.name + ' for the mParticle SDK' - } else { - description = target.mparticle.kitDescription - } - url = 'https://github.com/mparticle/mparticle-sdk-android' - licenses { - license { - name = 'The Apache Software License, Version 2.0' - url = 'https://www.apache.org/license/LICENSE-2.0.txt' - } - } - developers { - developer { - id = 'mParticle' - name = 'mParticle Inc.' - email = 'developers@mparticle.com' - } - } - scm { - url = 'https://github.com/mparticle/mparticle-android-sdk' - connection = 'scm:git:https://github.com/mparticle/mparticle-android-sdk' - developerConnection = 'scm:git:git@github.com:mparticle/mparticle-android-sdk.git' - } - } + private static void configureRepositories(RepositoryHandler repositories, boolean mparticleFromMavenLocalOnly) { + if (mparticleFromMavenLocalOnly) { + // Root build scripts may have already attached unfiltered repos. Rebuild this + // project's repository chain so com.mparticle cannot resolve outside mavenLocal(). + def existingRepos = repositories.toList() + existingRepos.each { repo -> + repositories.remove(repo) } - - publishing.repositories.maven { - credentials { - username System.getenv('sonatypeUsername') ?: "" - password System.getenv('sonatypePassword') ?: "" + repositories.mavenLocal { + content { + includeGroup 'com.mparticle' } - url = 'https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/' } - - target.extensions.configure(SigningExtension) { signing -> - signing.required { - target.gradle.taskGraph.hasTask("publishReleasePublicationToMavenRepository") - } - - def signingKey = target.findProperty('signingKey') ?: System.getenv('mavenSigningKeyId') - def signingPassword = target.findProperty('signingPassword') ?: System.getenv('mavenSigningKeyPassword') - - if (signingKey && signingPassword) { - signing.useInMemoryPgpKeys(signingKey, signingPassword) - signing.sign(target.extensions.getByType(PublishingExtension) - .publications - .findByName("release")) + repositories.google() + repositories.mavenCentral { + content { + excludeGroup 'com.mparticle' } } + } else { + repositories.mavenLocal() + repositories.google() + repositories.mavenCentral() } - - //Publishing task aliases for simpler local development - target.task("publishLocal") { dependsOn "publishDebugPublicationToMavenLocal" } - target.task("publishReleaseLocal") { dependsOn "publishReleasePublicationToMavenLocal" } } } diff --git a/kits/adjust-kit b/kits/adjust-kit deleted file mode 160000 index a66b47146..000000000 --- a/kits/adjust-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a66b47146100d32ed497b760d5e776125383a3a8 diff --git a/kits/adjust/adjust-5/README.md b/kits/adjust/adjust-5/README.md new file mode 100644 index 000000000..3bec0ec48 --- /dev/null +++ b/kits/adjust/adjust-5/README.md @@ -0,0 +1,24 @@ +# Adjust Kit Integration + +This repository contains the [Adjust](https://www.adjust.com/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-adjust-kit:5+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Adjust detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. + +## Documentation + +[Adjust integration](https://docs.mparticle.com/integrations/adjust/event/) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/adjust/adjust-5/build.gradle b/kits/adjust/adjust-5/build.gradle new file mode 100644 index 000000000..d828c3600 --- /dev/null +++ b/kits/adjust/adjust-5/build.gradle @@ -0,0 +1,62 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-adjust" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.adjustkit' + buildFeatures { + buildConfig = true + } + defaultConfig { + minSdkVersion 21 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } +} + +dependencies { + testImplementation files('libs/java-json.jar') + api 'com.adjust.sdk:adjust-android:[5.0.0,6.0.0)' +} diff --git a/kits/adjust/adjust-5/consumer-proguard.pro b/kits/adjust/adjust-5/consumer-proguard.pro new file mode 100644 index 000000000..35bdef0ba --- /dev/null +++ b/kits/adjust/adjust-5/consumer-proguard.pro @@ -0,0 +1,17 @@ +# These are the proguard rules specified by the Adjust SDK's documentation + +-keep class com.adjust.sdk.plugin.MacAddressUtil { java.lang.String getMacAddress(android.content.Context); } +-keep class com.adjust.sdk.plugin.AndroidIdUtil { java.lang.String getAndroidId(android.content.Context); } + +-keep class com.google.android.gms.common.ConnectionResult { + int SUCCESS; +} + +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient { + com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo (android.content.Context); +} + +-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info { + java.lang.String getId (); + boolean isLimitAdTrackingEnabled(); +} \ No newline at end of file diff --git a/kits/adjust/adjust-5/example/example-kotlin/build.gradle b/kits/adjust/adjust-5/example/example-kotlin/build.gradle new file mode 100644 index 000000000..653835c08 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.adjust.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.adjust.example.kotlin' + minSdk 21 + targetSdk 35 + versionCode 1 + versionName '1.0' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation project(':kits:adjust:adjust-5') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/AndroidManifest.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/ExampleApplication.kt b/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..d84519703 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.adjust.example.kotlin + +import android.app.Application +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : Application() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/MainActivity.kt b/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..8bb6d6233 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adjust/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.adjust.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/colors.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/strings.xml b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..69175f3b1 --- /dev/null +++ b/kits/adjust/adjust-5/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Adjust Kit Kotlin Example + diff --git a/kits/adjust/adjust-5/gradle.properties b/kits/adjust/adjust-5/gradle.properties new file mode 100644 index 000000000..05f921cc3 --- /dev/null +++ b/kits/adjust/adjust-5/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true \ No newline at end of file diff --git a/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.jar b/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.properties b/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/adjust/adjust-5/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/adjust/adjust-5/gradlew b/kits/adjust/adjust-5/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/adjust/adjust-5/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/adjust/adjust-5/gradlew.bat b/kits/adjust/adjust-5/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/adjust/adjust-5/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/adjust/adjust-5/libs/java-json.jar b/kits/adjust/adjust-5/libs/java-json.jar new file mode 100755 index 000000000..2f211e366 Binary files /dev/null and b/kits/adjust/adjust-5/libs/java-json.jar differ diff --git a/kits/adjust/adjust-5/settings.gradle.kts b/kits/adjust/adjust-5/settings.gradle.kts new file mode 100644 index 000000000..ed92bbfad --- /dev/null +++ b/kits/adjust/adjust-5/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-adjust-kit" +include(":") diff --git a/kits/adjust/adjust-5/src/main/AndroidManifest.xml b/kits/adjust/adjust-5/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c4e6c98d7 --- /dev/null +++ b/kits/adjust/adjust-5/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/AdjustKit.kt b/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/AdjustKit.kt new file mode 100644 index 000000000..6db4eb07e --- /dev/null +++ b/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/AdjustKit.kt @@ -0,0 +1,163 @@ +package com.mparticle.kits + +import android.app.Activity +import android.app.Application +import android.app.Application.ActivityLifecycleCallbacks +import android.content.Context +import android.content.Intent +import android.os.Bundle +import com.adjust.sdk.Adjust +import com.adjust.sdk.AdjustAttribution +import com.adjust.sdk.AdjustConfig +import com.adjust.sdk.AdjustInstance +import com.adjust.sdk.AdjustReferrerReceiver +import com.adjust.sdk.LogLevel +import com.adjust.sdk.OnAttributionChangedListener +import com.mparticle.AttributionError +import com.mparticle.AttributionResult +import com.mparticle.MParticle +import org.json.JSONException +import org.json.JSONObject +import java.util.LinkedList + +/** + * Embedded implementation of the Adjust SDK + * + * + */ +class AdjustKit : + KitIntegration(), + OnAttributionChangedListener, + ActivityLifecycleCallbacks { + override fun getInstance(): AdjustInstance = Adjust.getDefaultInstance() + + override fun getName(): String = KIT_NAME + + override fun onKitCreate( + settings: Map, + context: Context, + ): List { + val production = MParticle.Environment.Production == MParticle.getInstance()?.environment + val config = + AdjustConfig( + getContext(), + getSettings()[APP_TOKEN], + if (production) AdjustConfig.ENVIRONMENT_PRODUCTION else AdjustConfig.ENVIRONMENT_SANDBOX, + ) + config.setOnAttributionChangedListener(this) + if (deeplinkResponseListenerProxy != null) { + val listener = deeplinkResponseListenerProxy + if (listener != null) { + config.setOnDeferredDeeplinkResponseListener { deeplink -> + listener.launchReceivedDeeplink( + deeplink, + ) + } + } + deeplinkResponseListenerProxy = null + } + if (!production) { + config.setLogLevel(LogLevel.VERBOSE) + } + val fbAppId = getSettings()[FB_APP_ID_KEY] + if (fbAppId != null) { + config.setFbAppId(fbAppId) + } + Adjust.initSdk(config) + setAdidIntegrationAttribute() + (context.applicationContext as Application).registerActivityLifecycleCallbacks(this) + return emptyList() + } + + override fun setInstallReferrer(intent: Intent) { + AdjustReferrerReceiver().onReceive(context, intent) + } + + override fun setOptOut(optOutStatus: Boolean): List { + if (!optOutStatus) Adjust.enable() else Adjust.disable() + val messageList: MutableList = LinkedList() + messageList.add( + ReportingMessage( + this, + ReportingMessage.MessageType.OPT_OUT, + System.currentTimeMillis(), + null, + ).setOptOut(optOutStatus), + ) + return messageList + } + + override fun onAttributionChanged(attribution: AdjustAttribution) { + // if Attribution has not been fetch yet the argument + // will be null, in this case we should do nothing and wait for + // the asynchronous callback to return + var jsonObject = JSONObject() + try { + jsonObject = toJSON(attribution) + } catch (e: JSONException) { + val error = + AttributionError() + .setMessage(e.message) + .setServiceProviderId(MParticle.ServiceProviders.ADJUST) + kitManager.onError(error) + } + val deepLinkResult = + AttributionResult() + .setParameters(jsonObject) + .setServiceProviderId(MParticle.ServiceProviders.ADJUST) + kitManager.onResult(deepLinkResult) + } + + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle?, + ) {} + + override fun onActivityStarted(activity: Activity) {} + + override fun onActivityResumed(activity: Activity) { + } + + override fun onActivityPaused(activity: Activity) { + } + + override fun onActivityStopped(activity: Activity) {} + + override fun onActivitySaveInstanceState( + activity: Activity, + outState: Bundle, + ) {} + + override fun onActivityDestroyed(activity: Activity) {} + + private fun setAdidIntegrationAttribute() { + val integrationAttributes = integrationAttributes + Adjust.getAdid { adid -> + if (adid != null) { + integrationAttributes[ADJUST_ID_KEY] = adid + setIntegrationAttributes(integrationAttributes) + } + } + } + + companion object { + private const val APP_TOKEN = "appToken" + private const val ADJUST_ID_KEY = "adid" + private const val FB_APP_ID_KEY = "fbAppId" + private const val KIT_NAME = "Adjust" + + var deeplinkResponseListenerProxy: OnDeeplinkEventListener? = null + + @JvmStatic + @Throws(JSONException::class) + fun toJSON(attribution: AdjustAttribution): JSONObject = + JSONObject() + .putOpt("tracker_token", attribution.trackerToken) + .putOpt("tracker_name", attribution.trackerName) + .putOpt("network", attribution.network) + .putOpt("campaign", attribution.campaign) + .putOpt("adgroup", attribution.adgroup) + .putOpt("creative", attribution.creative) + .putOpt("click_label", attribution.clickLabel) + } +} diff --git a/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/OnDeeplinkEventListener.kt b/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/OnDeeplinkEventListener.kt new file mode 100644 index 000000000..12c1fe7fe --- /dev/null +++ b/kits/adjust/adjust-5/src/main/kotlin/com/mparticle/kits/OnDeeplinkEventListener.kt @@ -0,0 +1,7 @@ +package com.mparticle.kits + +import android.net.Uri + +interface OnDeeplinkEventListener { + fun launchReceivedDeeplink(deeplink: Uri?): Boolean +} diff --git a/kits/adjust/adjust-5/src/test/kotlin/com/mparticle/kits/AdjustKitTests.kt b/kits/adjust/adjust-5/src/test/kotlin/com/mparticle/kits/AdjustKitTests.kt new file mode 100644 index 000000000..371044780 --- /dev/null +++ b/kits/adjust/adjust-5/src/test/kotlin/com/mparticle/kits/AdjustKitTests.kt @@ -0,0 +1,78 @@ +package com.mparticle.kits + +import android.content.Context +import com.mparticle.MParticleOptions +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito + +class AdjustKitTests { + private val kit: KitIntegration + get() = AdjustKit() + + @Test + @Throws(Exception::class) + fun testGetName() { + val name = kit.name + Assert.assertTrue(!name.isNullOrEmpty()) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + @Throws(Exception::class) + fun testOnKitCreate() { + var e: Exception? = null + try { + val kit = kit + val settings = HashMap() + settings["fake setting"] = "fake" + val appToken = AdjustKit::class.java.getDeclaredField("APP_TOKEN") + appToken.isAccessible = true + val token = appToken.get(AdjustKit.Companion) as String + settings[token] = "test1" + val fbAppId = AdjustKit::class.java.getDeclaredField("FB_APP_ID_KEY") + fbAppId.isAccessible = true + val fbId = fbAppId.get(AdjustKit.Companion) as String + settings[fbId] = "test2" + kit.onKitCreate(settings, Mockito.mock(Context::class.java)) + } catch (ex: Exception) { + e = ex + } + Assert.assertNotNull(e) + } + + @Test + @Throws(Exception::class) + fun testClassName() { + val options = Mockito.mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = kit.javaClass.name + for (integration in integrations) { + if (integration.name == className) { + return + } + } + Assert.fail("$className not found as a known integration.") + } + + @get:Throws(JSONException::class) + private val attributionJSON: JSONObject + get() { + val jsonObject = JSONObject() + jsonObject.putOpt("tracker_token", "a1") + jsonObject.putOpt("tracker_name", "b2") + jsonObject.putOpt("network", "c3") + jsonObject.putOpt("campaign", "d4") + jsonObject.putOpt("adgroup", "e5") + jsonObject.putOpt("creative", "f6") + jsonObject.putOpt("click_label", "g7") + jsonObject.putOpt("adid", "h8") + return jsonObject + } +} diff --git a/kits/adobe-kit b/kits/adobe-kit deleted file mode 160000 index f149b654e..000000000 --- a/kits/adobe-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f149b654e19086e7f1e16e6a34401a3922efc647 diff --git a/kits/adobe/adobe/README.md b/kits/adobe/adobe/README.md new file mode 100644 index 000000000..3874c7f53 --- /dev/null +++ b/kits/adobe/adobe/README.md @@ -0,0 +1,25 @@ +# Adobe Kit Integration + +This repository contains the Adobe integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-adobe-kit:5+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Adobe detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. +4. Reference Adobe's integration doc below for more features. + +## Documentation + +[Adobe integration](https://docs.mparticle.com/integrations/amc/event/) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/adobe/adobe/build.gradle b/kits/adobe/adobe/build.gradle new file mode 100644 index 000000000..7f197e3cb --- /dev/null +++ b/kits/adobe/adobe/build.gradle @@ -0,0 +1,51 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-adobe" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.adjustkit' + defaultConfig { + minSdkVersion 16 + } + buildFeatures { + buildConfig = true + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } +} diff --git a/kits/adobe/adobe/consumer-proguard.pro b/kits/adobe/adobe/consumer-proguard.pro new file mode 100644 index 000000000..9aa9a40e3 --- /dev/null +++ b/kits/adobe/adobe/consumer-proguard.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/wpassidomo/Library/Android/sdk/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: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/kits/adobe/adobe/example/example-kotlin/build.gradle b/kits/adobe/adobe/example/example-kotlin/build.gradle new file mode 100644 index 000000000..74f4dbaa0 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.adobe.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.adobe.example.kotlin' + minSdk 16 + targetSdk 35 + versionCode 1 + versionName '1.0' + multiDexEnabled true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation project(':kits:adobe:adobe') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.multidex:multidex:2.0.1' +} diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/AndroidManifest.xml b/kits/adobe/adobe/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/ExampleApplication.kt b/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..409581228 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.adobe.example.kotlin + +import androidx.multidex.MultiDexApplication +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : MultiDexApplication() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/MainActivity.kt b/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..077db5600 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobe/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.adobe.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/values/colors.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/adobe/adobe/example/example-kotlin/src/main/res/values/strings.xml b/kits/adobe/adobe/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..001176bdb --- /dev/null +++ b/kits/adobe/adobe/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Adobe Kit Kotlin Example + diff --git a/kits/adobe/adobe/gradle.properties b/kits/adobe/adobe/gradle.properties new file mode 100644 index 000000000..edb1202c3 --- /dev/null +++ b/kits/adobe/adobe/gradle.properties @@ -0,0 +1,4 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2560m \ No newline at end of file diff --git a/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.jar b/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.properties b/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/adobe/adobe/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/adobe/adobe/gradlew b/kits/adobe/adobe/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/adobe/adobe/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/adobe/adobe/gradlew.bat b/kits/adobe/adobe/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/adobe/adobe/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/adobe/adobe/settings.gradle.kts b/kits/adobe/adobe/settings.gradle.kts new file mode 100644 index 000000000..4c0d8559d --- /dev/null +++ b/kits/adobe/adobe/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-adobe-kit" +include(":") diff --git a/kits/adobe/adobe/src/main/AndroidManifest.xml b/kits/adobe/adobe/src/main/AndroidManifest.xml new file mode 100644 index 000000000..94cbbcfc3 --- /dev/null +++ b/kits/adobe/adobe/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeApi.kt b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeApi.kt new file mode 100644 index 000000000..ea1fe1eb1 --- /dev/null +++ b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeApi.kt @@ -0,0 +1,5 @@ +package com.mparticle.kits + +class AdobeApi( + val marketingCloudID: String?, +) diff --git a/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKit.kt b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKit.kt new file mode 100644 index 000000000..966dd9d5c --- /dev/null +++ b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKit.kt @@ -0,0 +1,5 @@ +package com.mparticle.kits + +class AdobeKit : AdobeKitBase() { + override fun getName(): String = "Adobe" +} diff --git a/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKitBase.kt b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKitBase.kt new file mode 100644 index 000000000..bee7afe71 --- /dev/null +++ b/kits/adobe/adobe/src/main/kotlin/com/mparticle/kits/AdobeKitBase.kt @@ -0,0 +1,355 @@ +package com.mparticle.kits + +import android.content.Context +import android.content.Intent +import com.mparticle.MParticle.IdentityType +import com.mparticle.internal.MPUtility +import com.mparticle.internal.MPUtility.AdIdInfo +import com.mparticle.kits.KitIntegration.ApplicationStateListener +import com.mparticle.consent.ConsentState +import com.mparticle.kits.KitIntegration.ModifyIdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener +import com.mparticle.kits.KitIntegration.LogoutListener +import com.mparticle.kits.KitIntegration.PushListener +import org.json.JSONException +import org.json.JSONObject +import java.net.HttpURLConnection +import java.net.URL + +abstract class AdobeKitBase : + KitIntegration(), + UserAttributeListener, + ModifyIdentityListener, + LogoutListener, + PushListener, + ApplicationStateListener { + private val dVer = "2" + var url: String? = DEFAULT_URL + private var mOrgId: String? = null + private var requestInProgress = false + + @Throws(IllegalArgumentException::class) + public override fun onKitCreate( + map: Map, + context: Context, + ): List { + mOrgId = map[ORG_ID_KEY] + if (map.containsKey(AUDIENCE_MANAGER_SERVER)) { + url = map[AUDIENCE_MANAGER_SERVER] + } + syncIds() + return emptyList() + } + + override fun setOptOut(optedOut: Boolean): List = emptyList() + + override fun onApplicationForeground() { + syncIds() + } + + override fun onApplicationBackground() { + syncIds() + } + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, + ) { + syncIds() + } + + override fun supportsAttributeLists(): Boolean = false + + override fun onSetAllUserAttributes( + userAttributes: Map, + userAttributeLists: Map>, + ) { + syncIds() + } + + override fun onRemoveUserAttribute( + key: String, + ) { + syncIds() + } + + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + if (key == null || value == null || value !is String) { + return + } + syncIds() + } + + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + } + + override fun onSetUserTag( + key: String?, + ) { + } + + override fun onConsentStateUpdated( + oldState: ConsentState?, + newState: ConsentState?, + ) { + } + + override fun setUserIdentity( + identityType: IdentityType, + s: String, + ) { + syncIds() + } + + override fun removeUserIdentity(identityType: IdentityType) { + syncIds() + } + + override fun logout(): List = emptyList() + + override fun willHandlePushMessage(intent: Intent): Boolean = false + + override fun onPushMessageReceived( + context: Context, + intent: Intent, + ) { + // No-op: this kit does not implement push message handling. + } + + override fun onPushRegistration( + instanceId: String, + senderId: String, + ): Boolean { + syncIds() + return false + } + + @Synchronized + private fun syncIds() { + if (this.requestInProgress) return + requestInProgress = true + executeNetworkRequest { + try { + val url = URL("https", url, "/id?" + encodeIds()) + val urlConnection = url.openConnection() as HttpURLConnection + urlConnection.connectTimeout = 2000 + urlConnection.readTimeout = 10000 + if (urlConnection.responseCode in 200..299) { + val response = MPUtility.getJsonResponse(urlConnection) + parseResponse(response) + } + } catch (e: Exception) { + e.printStackTrace() + } + requestInProgress = false + } + } + + private fun encodeIds(): String { + var gaid: String? = null + val adId = MPUtility.getAdIdInfo(context) + if (adId != null && adId.advertiser == AdIdInfo.Advertiser.GOOGLE) { + gaid = adId.id + } + val pushId = kitManager.pushInstanceId + val dBlob = dBlob + val dcsRegion = dcsRegion + return encodeIds( + marketingCloudId, + mOrgId, + dBlob, + dcsRegion, + pushId, + gaid, + currentUser?.userIdentities ?: emptyMap(), + ) + } + + fun encodeIds( + marketingCloudId: String?, + orgId: String?, + dBlob: String?, + dcsRegion: String?, + pushId: String?, + gaid: String?, + userIdentities: Map, + ): String { + val builder = UrlBuilder() + builder + .append( + D_MID_KEY, + marketingCloudId, + ).append( + D_ORIG_ID_KEY, + orgId, + ).append( + D_BLOB_KEY, + dBlob, + ).append( + DCS_REGION_KEY, + dcsRegion, + ).append( + D_PLATFORM_KEY, + "android", + ).append( + D_VER, + dVer, + ).appendCustomIdentity( + PUSH_TOKEN_KEY, + pushId, + ).appendCustomIdentity( + GOOGLE_AD_ID_KEY, + gaid, + ) + for ((key, value) in userIdentities) { + builder.appendCustomIdentity( + getServerString(key), + value, + ) + } + return builder.toString() + } + + override fun getInstance(): Any = AdobeApi(marketingCloudId) + + private fun parseResponse(jsonObject: JSONObject) { + try { + val marketingCloudIdKey = jsonObject.getString(D_MID_KEY) + val dcsRegion = jsonObject.optString(DCS_REGION_KEY) + val dBlob = jsonObject.optString(D_BLOB_KEY) + marketingCloudId = marketingCloudIdKey + setDcsRegion(dcsRegion) + setDBlob(dBlob) + } catch (e: JSONException) { + e.printStackTrace() + } + } + + /** + * fetch the MarketingCloudId. If it can't be found in our storage, assume that this + * user is migrating from the Adobe SDK and try to fetch it from where the Adobe SDK would store it + */ + private var marketingCloudId: String? + get() { + var marketingCloudIdKey = integrationAttributes[MARKETING_CLOUD_ID_KEY] + if (KitUtils.isEmpty(marketingCloudIdKey)) { + var adobeSharedPrefs = + context.getSharedPreferences("visitorIDServiceDataStore", Context.MODE_PRIVATE) + marketingCloudIdKey = adobeSharedPrefs.getString("ADOBEMOBILE_PERSISTED_MID", null) + if (KitUtils.isEmpty(marketingCloudIdKey)) { + adobeSharedPrefs = + context.getSharedPreferences("APP_MEASUREMENT_CACHE", Context.MODE_PRIVATE) + marketingCloudIdKey = adobeSharedPrefs.getString("ADBMOBILE_PERSISTED_MID", null) + } + if (!KitUtils.isEmpty(marketingCloudIdKey)) { + marketingCloudId = marketingCloudIdKey + } + } + return marketingCloudIdKey + } + + private set(id) { + val integrationAttributes = integrationAttributes + if (!id.isNullOrEmpty() && !id.equals(integrationAttributes[MARKETING_CLOUD_ID_KEY])) { + integrationAttributes[MARKETING_CLOUD_ID_KEY] = id + setIntegrationAttributes(integrationAttributes) + val adobeSharedPrefs = + context.getSharedPreferences("visitorIDServiceDataStore", Context.MODE_PRIVATE) + adobeSharedPrefs.edit().putString("ADOBEMOBILE_PERSISTED_MID", id).apply() + } + } + + private val dcsRegion: String? + get() = integrationAttributes[AUDIENCE_MANAGER_LOCATION_HINT] + + private fun setDcsRegion(dcsRegion: String) { + val attrs = integrationAttributes + attrs[AUDIENCE_MANAGER_LOCATION_HINT] = dcsRegion + integrationAttributes = attrs + } + + private val dBlob: String? + get() = integrationAttributes[AUDIENCE_MANAGER_BLOB] + + private fun setDBlob(dBlob: String) { + val attrs = integrationAttributes + attrs[AUDIENCE_MANAGER_BLOB] = dBlob + integrationAttributes = attrs + } + + private inner class UrlBuilder { + var builder: StringBuilder = StringBuilder() + var hasValue = false + + fun append( + key: String?, + value: String?, + ): UrlBuilder { + if (KitUtils.isEmpty(key) || KitUtils.isEmpty(value)) { + return this + } + if (hasValue) { + builder.append("&") + } else { + hasValue = true + } + builder.append(key) + builder.append("=") + builder.append(value) + return this + } + + fun appendCustomIdentity( + key: Int, + value: String?, + ): UrlBuilder = append("d_cid", "$key%01$value") + + fun appendCustomIdentity( + key: String, + value: String, + ): UrlBuilder = append("d_cid_ic", "$key%01$value") + + override fun toString(): String = builder.toString() + } + + companion object { + const val MARKETING_CLOUD_ID_KEY = "mid" + private const val ORG_ID_KEY = "organizationID" + private const val AUDIENCE_MANAGER_BLOB = "aamb" + private const val AUDIENCE_MANAGER_LOCATION_HINT = "aamlh" + const val AUDIENCE_MANAGER_SERVER = "audienceManagerServer" + private const val D_MID_KEY = "d_mid" + private const val D_ORIG_ID_KEY = "d_orgid" + private const val D_BLOB_KEY = "d_blob" + private const val DCS_REGION_KEY = "dcs_region" + private const val D_PLATFORM_KEY = "d_ptfm" + private const val D_VER = "d_ver" + private const val DEFAULT_URL = "dpm.demdex.net" + private const val PUSH_TOKEN_KEY = 20919 + private const val GOOGLE_AD_ID_KEY = 20914 + + // TODO + // check if these are actually correct and replace + private fun getServerString(identityType: IdentityType): String = + when (identityType) { + IdentityType.Other -> "other" + IdentityType.CustomerId -> "customerid" + IdentityType.Facebook -> "facebook" + IdentityType.Twitter -> "twitter" + IdentityType.Google -> "google" + IdentityType.Microsoft -> "microsoft" + IdentityType.Yahoo -> "yahoo" + IdentityType.Email -> "email" + IdentityType.Alias -> "alias" + IdentityType.FacebookCustomAudienceId -> "facebookcustomaudienceid" + else -> "" + } + } +} diff --git a/kits/adobe/adobe/src/main/res/values/strings.xml b/kits/adobe/adobe/src/main/res/values/strings.xml new file mode 100644 index 000000000..8470bfa52 --- /dev/null +++ b/kits/adobe/adobe/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + adobe-kit + diff --git a/kits/adobe/adobe/src/test/kotlin/com/mparticle/kits/AdobeKitTest.kt b/kits/adobe/adobe/src/test/kotlin/com/mparticle/kits/AdobeKitTest.kt new file mode 100644 index 000000000..cdacbc47d --- /dev/null +++ b/kits/adobe/adobe/src/test/kotlin/com/mparticle/kits/AdobeKitTest.kt @@ -0,0 +1,148 @@ +package com.mparticle.kits + +import android.content.Context +import com.mparticle.MParticle +import com.mparticle.MParticleOptions +import com.mparticle.kits.AdobeKitBase +import com.mparticle.kits.KitIntegration +import com.mparticle.kits.KitIntegrationFactory +import com.mparticle.kits.KitManagerImpl +import org.junit.Assert +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito +import java.util.Arrays +import java.util.HashMap + +class AdobeKitTest { + private val kit: AdobeKit + get() = AdobeKit() + + @Test + fun testGetName() { + val name = kit.name + assertTrue(name.isNotEmpty()) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + fun testOnKitCreate() { + var e: Exception? = null + try { + val kit = kit + val settings = HashMap() + settings["fake setting"] = "fake" + kit.onKitCreate(settings, Mockito.mock(Context::class.java)) + } catch (ex: Exception) { + e = ex + } + + assertNotNull(e) + } + + @Test + fun testClassName() { + val options = Mockito.mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = kit.javaClass.name + for (integration in integrations) { + if (integration.name == className) { + return + } + } + Assert.fail("$className not found as a known integration.") + } + + @Test + fun testBuildUrl() { + val url = + kit.encodeIds( + "", + "", + "", + "", + "", + "", + HashMap(), + ) + val testUrl1 = + "d_mid=&d_ver=2&d_orgid=&d_cid=20914%01&d_cid=20919%01&dcs_region=&d_blob=&d_ptfm=android" + assertEqualUnorderedUrlParams(url, testUrl1) + + val userIdentities = HashMap() + userIdentities[MParticle.IdentityType.CustomerId] = "" + userIdentities[MParticle.IdentityType.Email] = "" + val url2 = + kit.encodeIds( + "", + "", + "", + "", + "", + "", + userIdentities, + ) + val testUrls2 = + "d_mid=&d_ver=2&d_orgid=&d_cid=20914%01&d_cid=20919%01&dcs_region=&d_blob=&d_ptfm=android&d_cid_ic=customerid%01&d_cid_ic=email%01" + assertEqualUnorderedUrlParams(url2, testUrls2) + } + + @Test + fun testGetUrlInstance() { + val kit = kit + kit.kitManager = Mockito.mock(KitManagerImpl::class.java) + val integrationAttributes = HashMap() + integrationAttributes[AdobeKitBase.MARKETING_CLOUD_ID_KEY] = "foo" + Mockito + .`when`( + kit.kitManager + .getIntegrationAttributes( + Mockito.any(KitIntegration::class.java), + ), + ).thenReturn(integrationAttributes) + + val settings = HashMap() + settings[AdobeKitBase.AUDIENCE_MANAGER_SERVER] = "some.random.url" + kit.onKitCreate(settings, Mockito.mock(Context::class.java)) + + val url = kit.url + assertEquals(url, "some.random.url") + } + + private fun assertEqualUnorderedUrlParams( + url1: String?, + url2: String?, + ) { + if (url1 == null && url2 == null) { + return + } + val url1Split = + Arrays.asList( + *url1!! + .split("&".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray(), + ) + val url2Split = + Arrays.asList( + *url2!! + .split("&".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray(), + ) + assertEquals(url1Split.size.toLong(), url2Split.size.toLong()) + url1Split.sort() + url2Split.sort() + for (i in url1Split.indices) { + if (url1Split[i] != url2Split[i]) { + assertTrue(false) + } + } + } +} diff --git a/kits/adobemedia-kit b/kits/adobemedia-kit deleted file mode 160000 index bed25f5bd..000000000 --- a/kits/adobemedia-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit bed25f5bd4b80445618b59ce57d84beb6d508d97 diff --git a/kits/adobemedia/adobemedia-3/README.md b/kits/adobemedia/adobemedia-3/README.md new file mode 100644 index 000000000..b79a29f2e --- /dev/null +++ b/kits/adobemedia/adobemedia-3/README.md @@ -0,0 +1,26 @@ +# Adobe Media Kit Integration + +This repository contains the [Adobe Media](https://docs.adobe.com/content/help/en/media-analytics/using/media-overview.html) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +Please note that the `adobemedia-kit` contains all the functionality of the `adobe-kit`. The presence of both will result in Duplicate Class errors and a failed build due to the overlap. + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-adobemedia-kit:3+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Adobe Media detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. + +## Documentation + +[Example integration](https://github.com/mParticle/mparticle-media-samples/tree/master/android) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/adobemedia/adobemedia-3/build.gradle b/kits/adobemedia/adobemedia-3/build.gradle new file mode 100644 index 000000000..ee4368959 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/build.gradle @@ -0,0 +1,96 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-adobe-media" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.adobemedia' + buildFeatures { + buildConfig = true + } + defaultConfig { + minSdkVersion 21 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } + lint { + // Workaround for lint internal crash + abortOnError false + // Ignore obsolete custom lint checks from older fragment library + disable 'ObsoleteLintCustomCheck' + } +} + +repositories { + google() + mavenCentral() +} + +configurations.all { + resolutionStrategy { + force "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + force "org.jetbrains.kotlin:kotlin-stdlib-common:$kotlin_version" + // Force Kotlin compiler version to match stdlib + eachDependency { DependencyResolveDetails details -> + if (details.requested.group == 'org.jetbrains.kotlin' && details.requested.name.startsWith('kotlin-')) { + details.useVersion kotlin_version + } + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + api 'com.mparticle:android-media:1.5.0' + api platform('com.adobe.marketing.mobile:sdk-bom:[3.0.0,4.0.0)') + api 'com.adobe.marketing.mobile:core' + api 'com.adobe.marketing.mobile:analytics' + api 'com.adobe.marketing.mobile:media' + api 'com.adobe.marketing.mobile:userprofile' + api 'com.adobe.marketing.mobile:lifecycle' + api 'com.adobe.marketing.mobile:identity' + api 'com.adobe.marketing.mobile:signal' +} diff --git a/kits/adobemedia/adobemedia-3/consumer-proguard.pro b/kits/adobemedia/adobemedia-3/consumer-proguard.pro new file mode 100644 index 000000000..2a90e0f4c --- /dev/null +++ b/kits/adobemedia/adobemedia-3/consumer-proguard.pro @@ -0,0 +1 @@ +# TODO: Add rules to exclude your SDK classes from proguard in implementing apps \ No newline at end of file diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/build.gradle b/kits/adobemedia/adobemedia-3/example/example-kotlin/build.gradle new file mode 100644 index 000000000..410dfe526 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.adobemedia.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.adobemedia.example.kotlin' + minSdk 21 + targetSdk 35 + versionCode 1 + versionName '1.0' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation project(':kits:adobemedia:adobemedia-3') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/AndroidManifest.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/ExampleApplication.kt b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..8e6231881 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.adobemedia.example.kotlin + +import android.app.Application +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : Application() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/MainActivity.kt b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..f8eb7d322 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/adobemedia/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.adobemedia.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/colors.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/strings.xml b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..413bf2683 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Adobe Media Kit Kotlin Example + diff --git a/kits/adobemedia/adobemedia-3/gradle.properties b/kits/adobemedia/adobemedia-3/gradle.properties new file mode 100644 index 000000000..7f4668fd7 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/gradle.properties @@ -0,0 +1,3 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true diff --git a/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.jar b/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.properties b/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/adobemedia/adobemedia-3/gradlew b/kits/adobemedia/adobemedia-3/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/adobemedia/adobemedia-3/gradlew.bat b/kits/adobemedia/adobemedia-3/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/adobemedia/adobemedia-3/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/adobemedia/adobemedia-3/libs/java-json.jar b/kits/adobemedia/adobemedia-3/libs/java-json.jar new file mode 100755 index 000000000..2f211e366 Binary files /dev/null and b/kits/adobemedia/adobemedia-3/libs/java-json.jar differ diff --git a/kits/adobemedia/adobemedia-3/settings.gradle.kts b/kits/adobemedia/adobemedia-3/settings.gradle.kts new file mode 100644 index 000000000..0863f2755 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-adobemedia-kit" +include(":") diff --git a/kits/adobemedia/adobemedia-3/src/main/AndroidManifest.xml b/kits/adobemedia/adobemedia-3/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c4e6c98d7 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeApi.kt b/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeApi.kt new file mode 100644 index 000000000..d2074a474 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeApi.kt @@ -0,0 +1,5 @@ +package com.mparticle.kits + +class AdobeApi( + val marketingCloudID: String, +) diff --git a/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeKit.kt b/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeKit.kt new file mode 100644 index 000000000..7ac44696d --- /dev/null +++ b/kits/adobemedia/adobemedia-3/src/main/kotlin/com/mparticle/kits/AdobeKit.kt @@ -0,0 +1,442 @@ +package com.mparticle.kits + +import android.app.Application +import android.content.Context +import android.content.Intent +import com.adobe.marketing.mobile.Analytics +import com.adobe.marketing.mobile.Identity +import com.adobe.marketing.mobile.Lifecycle +import com.adobe.marketing.mobile.Media +import com.adobe.marketing.mobile.MediaConstants +import com.adobe.marketing.mobile.MediaConstants.AdMetadataKeys +import com.adobe.marketing.mobile.MediaConstants.VideoMetadataKeys +import com.adobe.marketing.mobile.MediaTracker +import com.adobe.marketing.mobile.MobileCore +import com.adobe.marketing.mobile.Signal +import com.adobe.marketing.mobile.UserProfile +import com.mparticle.BaseEvent +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.kits.KitIntegration.ApplicationStateListener +import com.mparticle.consent.ConsentState +import com.mparticle.kits.KitIntegration.ModifyIdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener +import com.mparticle.kits.KitIntegration.EventListener +import com.mparticle.kits.KitIntegration.LogoutListener +import com.mparticle.kits.KitIntegration.PushListener +import com.mparticle.media.events.ContentType +import com.mparticle.media.events.EventAttributes +import com.mparticle.media.events.MediaAd +import com.mparticle.media.events.MediaAdBreak +import com.mparticle.media.events.MediaAttributeKeys +import com.mparticle.media.events.MediaContent +import com.mparticle.media.events.MediaEvent +import com.mparticle.media.events.MediaEventName +import com.mparticle.media.events.MediaSegment +import com.mparticle.media.events.StreamType + +open class AdobeKit : + KitIntegration(), + EventListener, + UserAttributeListener, + ModifyIdentityListener, + LogoutListener, + PushListener, + ApplicationStateListener { + companion object { + internal const val MARKETING_CLOUD_ID_KEY = "mid" + internal const val LAUNCH_APP_ID = "launchAppId" + } + + var defaultMediaTracker: MediaTracker? = null + var mediaTrackers: MutableMap = mutableMapOf() + var currentPlayheadPosition: Long = 0 + + override fun getName() = "Adobe Media" + + public override fun onKitCreate( + settings: Map, + context: Context, + ): List { + val appId = settings.get(LAUNCH_APP_ID) + + MobileCore.setApplication(context.applicationContext as Application) + val extensions = + listOf( + Analytics.EXTENSION, + Media.EXTENSION, + UserProfile.EXTENSION, + Identity.EXTENSION, + Lifecycle.EXTENSION, + Signal.EXTENSION, + ) + appId?.let { + MobileCore.configureWithAppID(appId) + } + MobileCore.registerExtensions(extensions) { + syncIds() + } + defaultMediaTracker = Media.createTracker() + return listOf() + } + + override fun onApplicationForeground() { + syncIds() + } + + override fun onApplicationBackground() { + syncIds() + } + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, + ) { + syncIds() + } + + override fun supportsAttributeLists(): Boolean = false + + override fun onSetAllUserAttributes( + userAttributes: Map, + userAttributeLists: Map>, + ) { + syncIds() + } + + override fun onRemoveUserAttribute( + key: String, + ) { + syncIds() + } + + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + if (key == null || value == null || value !is String) { + return + } + syncIds() + } + + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + } + + override fun onSetUserTag( + key: String?, + ) { + } + + override fun onConsentStateUpdated( + oldState: ConsentState?, + newState: ConsentState?, + ) { + } + + override fun setUserIdentity( + identityType: MParticle.IdentityType, + s: String, + ) { + syncIds() + } + + override fun removeUserIdentity(identityType: MParticle.IdentityType) { + syncIds() + } + + override fun logout(): List = emptyList() + + override fun willHandlePushMessage(intent: Intent): Boolean = false + + override fun onPushMessageReceived( + context: Context, + intent: Intent, + ) {} + + override fun onPushRegistration( + instanceId: String, + senderId: String, + ): Boolean { + syncIds() + return false + } + + private fun syncIds() { + Identity.getExperienceCloudId { + setMarketingCloudId(it) + } + } + + override fun getInstance() = + Identity.getExperienceCloudId { + setMarketingCloudId(it) + AdobeApi(it) + } + + fun setMarketingCloudId(id: String) { + val integrationAttributes = integrationAttributes + if (id.length > 0 && !id.equals(integrationAttributes[MARKETING_CLOUD_ID_KEY])) { + integrationAttributes[MARKETING_CLOUD_ID_KEY] = id + setIntegrationAttributes(integrationAttributes) + } + } + + override fun setOptOut(optout: Boolean): List = emptyList() + + override fun logEvent(p0: MPEvent) = null + + override fun leaveBreadcrumb(p0: String?) = null + + override fun logException( + p0: Exception?, + p1: MutableMap?, + p2: String?, + ) = null + + override fun logScreen( + p0: String?, + p1: MutableMap?, + ) = null + + override fun logError( + errorString: String?, + p1: MutableMap?, + ): List? { + errorString?.let { + defaultMediaTracker?.trackError(errorString) + } + return null + } + + override fun logBaseEvent(event: BaseEvent): MutableList? { + if (event is MediaEvent) { + val sessionId = event.sessionId + event.playheadPosition?.let { + currentPlayheadPosition = it + sessionId?.let { id -> + mediaTrackers[id]?.updateCurrentPlayhead(it.toSeconds()) + } + } + when (event.eventName) { + MediaEventName.SESSION_START -> sessionStart(event) + MediaEventName.SESSION_END -> sessionEnd(event) + MediaEventName.PLAY -> play(event) + MediaEventName.PAUSE -> pause(event) + MediaEventName.AD_BREAK_END -> adBreakEnd(event) + MediaEventName.AD_BREAK_START -> adBreakStart(event) + MediaEventName.AD_START -> adStart(event) + MediaEventName.AD_SKIP, MediaEventName.AD_END -> adEnd(event) + MediaEventName.UPDATE_QOS -> updateQos(event) + MediaEventName.BUFFER_END -> bufferEnd(event) + MediaEventName.BUFFER_START -> bufferStart(event) + MediaEventName.SEEK_START -> seekStart(event) + MediaEventName.SEEK_END -> seekEnd(event) + MediaEventName.SEGMENT_START -> segmentStart(event) + MediaEventName.SEGMENT_SKIP -> segmentSkip(event) + MediaEventName.SEGMENT_END -> segmentEnd(event) + MediaEventName.UPDATE_PLAYHEAD_POSITION -> { + /** already handled */ + } + MediaEventName.AD_CLICK -> { + /** do nothing */ + } + } + } + return null + } + + private fun sessionStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val mediaInfo = mediaEvent.mediaContent.getMediaObject() + mediaTrackers[sessionId] = Media.createTracker() + mediaTrackers[sessionId]?.trackSessionStart(mediaInfo, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun sessionEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + mediaTrackers[sessionId]?.trackSessionEnd() + mediaTrackers.remove(sessionId) + } + + private fun play(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + mediaTrackers[sessionId]?.trackPlay() + } + + private fun pause(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + mediaTrackers[sessionId]?.trackPause() + } + + private fun updateQos(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + mediaEvent.qos?.let { mediaQos -> + val qoe = + Media.createQoEObject( + mediaQos.bitRate?.toLong() ?: 0, + mediaQos.startupTime?.toSeconds() ?: 0.0, + mediaQos.fps?.toDouble() ?: 0.0, + mediaQos.droppedFrames?.toLong() ?: 0, + ) + mediaTrackers[sessionId]?.updateQoEObject(qoe) + } + } + + private fun adBreakStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val adBreakObject = mediaEvent.adBreak?.getAdBreakObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.AdBreakStart, adBreakObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun adBreakEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val adBreakObject = mediaEvent.adBreak?.getAdBreakObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.AdBreakComplete, adBreakObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun adStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val adBreakObject = mediaEvent.mediaAd?.getAdObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.AdStart, adBreakObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun adEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + mediaTrackers[sessionId]?.trackEvent( + Media.Event.AdComplete, + mediaEvent.mediaAd?.getAdObject(), + mediaEvent.customAttributes?.toAdobeAttributes(), + ) + } + + private fun seekEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val mediaObject = mediaEvent.mediaContent.getMediaObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.SeekComplete, mediaObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun seekStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val mediaObject = mediaEvent.mediaContent.getMediaObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.SeekStart, mediaObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun bufferEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val mediaObject = mediaEvent.mediaContent.getMediaObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.BufferComplete, mediaObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun bufferStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val mediaObject = mediaEvent.mediaContent.getMediaObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.BufferStart, mediaObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun segmentEnd(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val chapterObject = mediaEvent.segment?.getChapterObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.ChapterComplete, chapterObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun segmentSkip(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val chapterObject = mediaEvent.segment?.getChapterObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.ChapterSkip, chapterObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun segmentStart(mediaEvent: MediaEvent) { + val sessionId = mediaEvent.sessionId ?: return + val chapterObject = mediaEvent.segment?.getChapterObject() + mediaTrackers[sessionId]?.trackEvent(Media.Event.ChapterStart, chapterObject, mediaEvent.customAttributes?.toAdobeAttributes()) + } + + private fun MediaSegment.getChapterObject(): Map = + Media.createChapterObject( + title ?: "", + index?.toLong() ?: 0, + duration?.toDouble() ?: 0.0, + currentPlayheadPosition.toDouble(), + ) + + internal fun MediaContent.getMediaObject(): Map = + Media.createMediaObject( + name ?: "", + contentId ?: "", + duration?.toSeconds() ?: 0.0, + getStreamType() ?: "", + getMediaType(), + ) + + internal fun MediaAdBreak.getAdBreakObject(): Map = + Media.createAdBreakObject( + title ?: "", + 1L, + currentPlayheadPosition.toSeconds(), + ) + + internal fun MediaAd.getAdObject(): Map = + Media.createAdObject( + title ?: "", + id ?: "", + position?.toLong() ?: 0, + duration?.toDouble() ?: 0.0, + ) + + internal fun MediaContent.getMediaType(): Media.MediaType = + when (contentType) { + ContentType.AUDIO -> Media.MediaType.Audio + ContentType.VIDEO -> Media.MediaType.Video + // Adobe requires that this be non-nullable now, but it should never reach this else statement. + else -> Media.MediaType.Video + } + + internal fun MediaContent.getStreamType(): String? = + when (streamType) { + StreamType.LIVE_STEAM -> MediaConstants.StreamType.LIVE + StreamType.LINEAR -> MediaConstants.StreamType.LINEAR + StreamType.ON_DEMAND -> { + when (contentType) { + ContentType.AUDIO -> MediaConstants.StreamType.AOD + ContentType.VIDEO -> MediaConstants.StreamType.VOD + else -> null + } + } + StreamType.PODCAST -> MediaConstants.StreamType.PODCAST + StreamType.AUDIOBOOK -> MediaConstants.StreamType.AUDIOBOOK + else -> null + } + + internal fun Map.toAdobeAttributes(): Map = + entries.associate { (key, value) -> + when (key) { + MediaAttributeKeys.AD_ADVERTISING_ID -> AdMetadataKeys.ADVERTISER + MediaAttributeKeys.AD_CAMPAIGN -> AdMetadataKeys.CAMPAIGN_ID + MediaAttributeKeys.AD_CREATIVE -> AdMetadataKeys.CREATIVE_ID + MediaAttributeKeys.AD_PLACEMENT -> AdMetadataKeys.PLACEMENT_ID + MediaAttributeKeys.AD_SITE_ID -> AdMetadataKeys.SITE_ID + EventAttributes.CONTENT_SHOW -> VideoMetadataKeys.SHOW + EventAttributes.CONTENT_EPISODE -> VideoMetadataKeys.EPISODE + EventAttributes.CONTENT_ASSET_ID -> VideoMetadataKeys.ASSET_ID + EventAttributes.CONTENT_GENRE -> VideoMetadataKeys.GENRE + EventAttributes.CONTENT_FIRST_AIR_DATE -> VideoMetadataKeys.FIRST_AIR_DATE + EventAttributes.CONTENT_DIGITAL_DATE -> VideoMetadataKeys.FIRST_DIGITAL_DATE + EventAttributes.CONTENT_RATING -> VideoMetadataKeys.RATING + EventAttributes.CONTENT_ORIGINATOR -> VideoMetadataKeys.ORIGINATOR + EventAttributes.CONTENT_NETWORK -> VideoMetadataKeys.NETWORK + EventAttributes.CONTENT_SHOW_TYPE -> VideoMetadataKeys.SHOW_TYPE + EventAttributes.CONTENT_MVPD -> VideoMetadataKeys.MVPD + EventAttributes.CONTENT_AUTHORIZED -> VideoMetadataKeys.AUTHORIZED + EventAttributes.CONTENT_DAYPART -> VideoMetadataKeys.DAY_PART + EventAttributes.CONTENT_FEED -> VideoMetadataKeys.FEED + else -> key + } to value.toString() + } + + internal fun Long.toSeconds(): Double = toDouble() / 1000 +} diff --git a/kits/adobemedia/adobemedia-3/src/test/kotlin/com/adobe/marketing/mobile/MobileCore.kt b/kits/adobemedia/adobemedia-3/src/test/kotlin/com/adobe/marketing/mobile/MobileCore.kt new file mode 100644 index 000000000..8b2630caa --- /dev/null +++ b/kits/adobemedia/adobemedia-3/src/test/kotlin/com/adobe/marketing/mobile/MobileCore.kt @@ -0,0 +1,89 @@ +package com.adobe.marketing.mobile + +import android.app.Application + +class MobileCore { + companion object { + var configKey: String? = null + + @JvmStatic + fun start(callback: AdobeCallback) { + callback.call("") + } + + @JvmStatic + fun configureWithAppID(key: String) { + configKey = key + } + + @JvmStatic + fun registerExtensions( + extensions: List>, + completionCallback: AdobeCallback<*>?, + ) { + // Method implementation here + } + + @JvmStatic + fun setApplication(application: Application) {} + } +} + +interface AdobeCallback { + fun call(t: T) +} + +open class BaseAdobeExtension { + internal abstract class AnalyticsExtension( + extensionApi: ExtensionApi, + ) : com.adobe.marketing.mobile.Extension(extensionApi) { + companion object { + private val ANALYTICS_HARD_DEPENDENCIES: List = + listOf( + // Add actual hard dependencies here + ) + + private val ANALYTICS_SOFT_DEPENDENCIES: List = + listOf( + // Add actual soft dependencies here + ) + + private const val CLASS_NAME: String = "YourClassName" // Replace "YourClassName" with the actual class name + } + } + + companion object { + @JvmField + val EXTENSION: Class = AnalyticsExtension::class.java + + @JvmStatic + fun registerExtension() {} + } +} + +class MobileServices : BaseAdobeExtension() + +class Analytics : BaseAdobeExtension() + +class UserProfile : BaseAdobeExtension() + +class Lifecycle : BaseAdobeExtension() + +class Signal : BaseAdobeExtension() + +object Identity : BaseAdobeExtension() { + @JvmStatic + fun getExperienceCloudId(callback: AdobeCallback) {} +} + +object Media : BaseAdobeExtension() { + @JvmStatic + fun createTracker(): MediaTracker = MediaTracker() + + enum class MediaType { + Video, + Audio, + } +} + +open class MediaTracker diff --git a/kits/adobemedia/adobemedia-3/src/test/kotlin/com/mparticle/kits/AdobeMediaKitTests.kt b/kits/adobemedia/adobemedia-3/src/test/kotlin/com/mparticle/kits/AdobeMediaKitTests.kt new file mode 100644 index 000000000..a7935cbb6 --- /dev/null +++ b/kits/adobemedia/adobemedia-3/src/test/kotlin/com/mparticle/kits/AdobeMediaKitTests.kt @@ -0,0 +1,108 @@ +package com.mparticle.kits + +import android.app.Application +import android.content.Context +import com.adobe.marketing.mobile.Media +import com.adobe.marketing.mobile.MediaTracker +import com.adobe.marketing.mobile.MobileCore +import com.mparticle.MParticleOptions +import com.mparticle.media.events.ContentType +import com.mparticle.media.events.MediaContent +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.Mockito + +class AdobeMediaKitTests { + private val kit: AdobeKit + get() = + object : AdobeKit() { + val tracker: MediaTracker? + get() { + return super.defaultMediaTracker + } + } + + @Test + fun testGetName() { + val name = kit.name + assertTrue(name.isNotEmpty()) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + fun testOnKitCreate() { + var e: Exception? = null + try { + val kit: KitIntegration = kit + val settings = HashMap() + settings["fake setting"] = "fake" + kit.onKitCreate(settings, Mockito.mock(Context::class.java)) + } catch (ex: Exception) { + e = ex + } + assertNotNull(e) + } + + @Test + fun testClassName() { + val options = Mockito.mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = AdobeKit()::class.java.name + assertEquals("$className not found as a known integration.", 1, integrations.filter { it.name == className }.count()) + } + + @Test + fun testMediaConfig() { + val testKit = + object : AdobeKit() { + val tracker: MediaTracker? + get() { + return super.defaultMediaTracker + } + } + val trackingServer = "launch app idea" + val settings = mutableMapOf(AdobeKit.LAUNCH_APP_ID to trackingServer) + val context = Mockito.mock(Context::class.java) + Mockito.`when`(context.applicationContext).thenReturn(Mockito.mock(Application::class.java)) + + testKit.kitManager = Mockito.mock(KitManagerImpl::class.java) + Mockito.`when`(testKit.kitManager.getIntegrationAttributes(Mockito.any(KitIntegration::class.java))).thenReturn( + mapOf(AdobeKit.MARKETING_CLOUD_ID_KEY to "not nothing"), + ) + testKit.onKitCreate(settings, context) + + assertEquals(trackingServer, MobileCore.configKey) + assertNotNull(testKit.tracker) + } + + @Test + fun toSecondsTest() { + kit.apply { + assertEquals(1.001, 1001L.toSeconds(), 0.0) + } + } + + @Test + fun getMediaTypeTest() { + var mediaContent = + MediaContent().apply { + contentType = ContentType.AUDIO + } + kit.apply { + assertEquals(Media.MediaType.Audio, mediaContent.getMediaType()) + } + mediaContent = + MediaContent().apply { + contentType = ContentType.VIDEO + } + kit.apply { + assertEquals(Media.MediaType.Video, mediaContent.getMediaType()) + } + } +} diff --git a/kits/appboy-kit b/kits/appboy-kit deleted file mode 160000 index 4fd583e67..000000000 --- a/kits/appboy-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fd583e67b4b9688d383a8dfed1c315957ba35c8 diff --git a/kits/appsflyer-kit b/kits/appsflyer-kit deleted file mode 160000 index 84e55b8fa..000000000 --- a/kits/appsflyer-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 84e55b8fae69a07f091034fb530b87f5924adb7d diff --git a/kits/appsflyer/appsflyer-6/README.md b/kits/appsflyer/appsflyer-6/README.md new file mode 100644 index 000000000..81c0e52be --- /dev/null +++ b/kits/appsflyer/appsflyer-6/README.md @@ -0,0 +1,31 @@ +# AppsFlyer Kit Integration + +This repository contains the [AppsFlyer](https://www.appsflyer.com/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-appsflyer-kit:5+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"AppsFlyer detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. +4. If you wish to utilize Appsflyers InstallReferrer capabilities, add a dependency for Play Install Referrer library in you app's build.gradle. For more information visit the [Appsflyer SDK's documentation page](https://support.appsflyer.com/hc/en-us/articles/207032066#attribution) on the subject: + + ```groovy + dependencies { + implementation "com.android.installreferrer:installreferrer:2.2" + } + ``` + +## Documentation + +[AppsFlyer integration](http://docs.mparticle.com/integrations/appsflyer/event/) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/appsflyer/appsflyer-6/build.gradle b/kits/appsflyer/appsflyer-6/build.gradle new file mode 100644 index 000000000..b458b5995 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/build.gradle @@ -0,0 +1,69 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-appsflyer" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.appsflyer' + buildFeatures { + buildConfig = true + } + defaultConfig { + minSdkVersion 19 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } +} + +ktlint { + android.set(true) +} + +dependencies { + api 'com.appsflyer:af-android-sdk:[6.17.3,7.0.0)' + testImplementation files('libs/java-json.jar') + testImplementation files('libs/testutils.aar') +} +repositories { + mavenCentral() +} diff --git a/kits/appsflyer/appsflyer-6/consumer-proguard.pro b/kits/appsflyer/appsflyer-6/consumer-proguard.pro new file mode 100644 index 000000000..e69de29bb diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/build.gradle b/kits/appsflyer/appsflyer-6/example/example-kotlin/build.gradle new file mode 100644 index 000000000..2b03ec9b9 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/build.gradle @@ -0,0 +1,40 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.appsflyer.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.appsflyer.example.kotlin' + minSdk 19 + targetSdk 35 + versionCode 1 + versionName '1.0' + multiDexEnabled true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation project(':kits:appsflyer:appsflyer-6') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.multidex:multidex:2.0.1' +} diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/AndroidManifest.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/ExampleApplication.kt b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..18fd5ef8d --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.appsflyer.example.kotlin + +import androidx.multidex.MultiDexApplication +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : MultiDexApplication() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/MainActivity.kt b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..eb2766f09 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/appsflyer/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.appsflyer.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/colors.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/strings.xml b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..fa734d856 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + AppsFlyer Kit Kotlin Example + diff --git a/kits/appsflyer/appsflyer-6/gradle.properties b/kits/appsflyer/appsflyer-6/gradle.properties new file mode 100644 index 000000000..edb1202c3 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/gradle.properties @@ -0,0 +1,4 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2560m \ No newline at end of file diff --git a/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.jar b/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.properties b/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/appsflyer/appsflyer-6/gradlew b/kits/appsflyer/appsflyer-6/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/appsflyer/appsflyer-6/gradlew.bat b/kits/appsflyer/appsflyer-6/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/appsflyer/appsflyer-6/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/appsflyer/appsflyer-6/libs/java-json.jar b/kits/appsflyer/appsflyer-6/libs/java-json.jar new file mode 100755 index 000000000..2f211e366 Binary files /dev/null and b/kits/appsflyer/appsflyer-6/libs/java-json.jar differ diff --git a/kits/appsflyer/appsflyer-6/libs/test-utils.aar b/kits/appsflyer/appsflyer-6/libs/test-utils.aar new file mode 100644 index 000000000..a13382c7d Binary files /dev/null and b/kits/appsflyer/appsflyer-6/libs/test-utils.aar differ diff --git a/kits/appsflyer/appsflyer-6/libs/testutils.aar b/kits/appsflyer/appsflyer-6/libs/testutils.aar new file mode 100644 index 000000000..cf49b4189 Binary files /dev/null and b/kits/appsflyer/appsflyer-6/libs/testutils.aar differ diff --git a/kits/appsflyer/appsflyer-6/settings.gradle.kts b/kits/appsflyer/appsflyer-6/settings.gradle.kts new file mode 100644 index 000000000..64ca48ba7 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-appsflyer-kit" +include(":") diff --git a/kits/appsflyer/appsflyer-6/src/main/AndroidManifest.xml b/kits/appsflyer/appsflyer-6/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ec4a76765 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/appsflyer/appsflyer-6/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt b/kits/appsflyer/appsflyer-6/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt new file mode 100644 index 000000000..bf38ca82f --- /dev/null +++ b/kits/appsflyer/appsflyer-6/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt @@ -0,0 +1,631 @@ +package com.mparticle.kits + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.location.Location +import android.os.Bundle +import com.appsflyer.AFInAppEventParameterName.CONTENT_ID +import com.appsflyer.AFInAppEventParameterName.CONTENT_TYPE +import com.appsflyer.AFInAppEventParameterName.CURRENCY +import com.appsflyer.AFInAppEventParameterName.PRICE +import com.appsflyer.AFInAppEventParameterName.QUANTITY +import com.appsflyer.AFInAppEventParameterName.REVENUE +import com.appsflyer.AFInAppEventType +import com.appsflyer.AFInAppEventType.ADD_TO_CART +import com.appsflyer.AFInAppEventType.ADD_TO_WISH_LIST +import com.appsflyer.AFInAppEventType.INITIATED_CHECKOUT +import com.appsflyer.AFInAppEventType.PURCHASE +import com.appsflyer.AppsFlyerConsent +import com.appsflyer.AppsFlyerConversionListener +import com.appsflyer.AppsFlyerLib +import com.appsflyer.AppsFlyerProperties +import com.appsflyer.deeplink.DeepLinkListener +import com.appsflyer.deeplink.DeepLinkResult +import com.mparticle.AttributionError +import com.mparticle.AttributionResult +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.commerce.CommerceEvent +import com.mparticle.commerce.Product +import com.mparticle.consent.ConsentState +import com.mparticle.internal.Logger +import com.mparticle.internal.MPUtility +import com.mparticle.kits.KitIntegration.LogoutListener +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.math.BigDecimal +import java.util.LinkedList +import com.mparticle.kits.KitIntegration.ModifyIdentityListener + +/** + * mParticle Kit wrapper for the AppsFlyer SDK + */ +class AppsFlyerKit : + KitIntegration(), + KitIntegration.EventListener, + ModifyIdentityListener, + LogoutListener, + KitIntegration.CommerceListener, + AppsFlyerConversionListener, + KitIntegration.ActivityListener, + KitIntegration.UserAttributeListener { + override fun getInstance(): AppsFlyerLib = AppsFlyerLib.getInstance() + + override fun getName() = NAME + + public override fun onKitCreate( + setting: Map?, + context: Context, + ): List { + AppsFlyerLib + .getInstance() + .setDebugLog( + MParticle.getInstance()?.environment == MParticle.Environment.Development, + ) + settings[DEV_KEY]?.let { AppsFlyerLib.getInstance().init(it, this, context) } + setting?.get(SHARING_FILTER_FOR_PARTNERS)?.let { + applySharingFilterForPartners(it) + } + val userConsentState = currentUser?.consentState + setConsent(userConsentState) + AppsFlyerLib.getInstance().start(context.applicationContext) + AppsFlyerLib.getInstance().setCollectAndroidID(MParticle.isAndroidIdEnabled()) + val integrationAttributes = HashMap(1) + integrationAttributes[APPSFLYERID_INTEGRATION_KEY] = + AppsFlyerLib.getInstance().getAppsFlyerUID(context) + setIntegrationAttributes(integrationAttributes) + AppsFlyerLib.getInstance().subscribeForDeepLink(deepLinkListener()) + + val messages: MutableList = ArrayList() + messages.add( + ReportingMessage( + this, + ReportingMessage.MessageType.APP_STATE_TRANSITION, + System.currentTimeMillis(), + null, + ), + ) + return messages + } + + override fun leaveBreadcrumb(breadcrumb: String): List = emptyList() + + override fun logError( + message: String, + eventData: Map, + ): List = emptyList() + + override fun logException( + exception: Exception, + eventData: Map, + message: String, + ): List = emptyList() + + override fun logLtvIncrease( + valueIncreased: BigDecimal, + valueTotal: BigDecimal, + eventName: String, + contextInfo: Map, + ): List = emptyList() + + override fun logEvent(event: CommerceEvent): List { + val messages: MutableList = LinkedList() + val eventValues: MutableMap = HashMap() + val productList = event.products + + if (isSalesEvent(event)) { + logSalesEvent(event, eventValues, productList, messages) + } else { + logNotSalesEvent(event, messages) + } + return messages + } + + private fun logNotSalesEvent( + event: CommerceEvent, + messages: MutableList, + ) { + val eventList = CommerceEventUtils.expand(event) + if (eventList.isNotEmpty()) { + for (e in eventList) { + try { + logEvent(e) + messages.add(ReportingMessage.fromEvent(this, event)) + } catch (e: Exception) { + Logger.warning("Failed to call logCustomEvent to AppsFlyer kit: $e") + } + } + } + } + + private fun logSalesEvent( + event: CommerceEvent, + eventValues: MutableMap, + productList: MutableList?, + messages: MutableList, + ) { + event.customAttributes?.let { eventValues.putAll(it) } + + if (!KitUtils.isEmpty(event.currency)) { + eventValues[CURRENCY] = event.currency + } + + if (event.productAction == Product.ADD_TO_CART || + event.productAction == Product.ADD_TO_WISHLIST + ) { + val eventName = + if (event.productAction == Product.ADD_TO_CART) { + ADD_TO_CART + } else { + ADD_TO_WISH_LIST + } + + productList?.iterator()?.forEach { product -> + val productEventValues: MutableMap = hashMapOf() + productEventValues.putAll(eventValues) + with(product) { + productEventValues[PRICE] = unitPrice + productEventValues[QUANTITY] = quantity + if (!KitUtils.isEmpty(sku)) { + productEventValues[CONTENT_ID] = sku + } + if (!KitUtils.isEmpty(category)) { + productEventValues[CONTENT_TYPE] = category + } + } + instance.logEvent(context, eventName, productEventValues) + messages.add(ReportingMessage.fromEvent(this, event)) + } + } else { + val eventName = + if (event.productAction == Product.CHECKOUT) { + INITIATED_CHECKOUT + } else { + PURCHASE + } + eventValues[CONTENT_ID] = generateProductIdList(event) + + if (!productList.isNullOrEmpty()) { + var totalQuantity = 0.0 + for (product in productList) { + totalQuantity += product.quantity + } + eventValues[QUANTITY] = totalQuantity + } + + val transactionAttributes = event.transactionAttributes + if ((transactionAttributes != null) && (transactionAttributes.revenue != 0.0)) { + val revenue = transactionAttributes.revenue + if (event.productAction == Product.PURCHASE) { + eventValues[REVENUE] = revenue + if (!MPUtility.isEmpty(transactionAttributes.id)) { + eventValues[AFInAppEventType.ORDER_ID] = transactionAttributes.id + } + } else { + eventValues[PRICE] = revenue + } + } + instance.logEvent(context, eventName, eventValues) + messages.add(ReportingMessage.fromEvent(this, event)) + } + } + + private fun isSalesEvent(event: CommerceEvent) = + event.productAction == Product.ADD_TO_CART || + event.productAction == Product.ADD_TO_WISHLIST || + event.productAction == Product.CHECKOUT || + event.productAction == Product.PURCHASE + + override fun logEvent(event: MPEvent): List { + var hashMap: HashMap? = hashMapOf() + if (event.customAttributes?.isNotEmpty() == true) { + hashMap = event.customAttributes?.let { HashMap(it) } + } + instance.logEvent(context, event.eventName, hashMap) + val messages: MutableList = LinkedList() + messages.add(ReportingMessage.fromEvent(this, event)) + return messages + } + + override fun logScreen( + screenName: String, + eventAttributes: Map, + ): List = emptyList() + + override fun setOptOut(optOutStatus: Boolean): List { + instance.anonymizeUser(optOutStatus) + val messageList: MutableList = LinkedList() + messageList.add( + ReportingMessage( + this, + ReportingMessage.MessageType.OPT_OUT, + System.currentTimeMillis(), + null, + ).setOptOut(optOutStatus), + ) + return messageList + } + + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + } + + override fun onRemoveUserAttribute( + key: String?, + ) { + } + + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + // No-op: this kit does not implement this feature. + } + + override fun onSetUserTag( + key: String?, + ) { + } + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, + ) { + // not supported + } + + override fun onSetAllUserAttributes( + userAttributes: MutableMap?, + userAttributeLists: MutableMap>?, + ) { + // No-op: this kit does not implement this feature. + } + + override fun supportsAttributeLists(): Boolean = true + + override fun removeUserIdentity(identityType: MParticle.IdentityType) { + with(instance) { + if (MParticle.IdentityType.CustomerId == identityType) { + setCustomerUserId("") + } else if (MParticle.IdentityType.Email == identityType) { + setUserEmails(AppsFlyerProperties.EmailsCryptType.NONE, "") + } + } + } + + override fun setUserIdentity( + identityType: MParticle.IdentityType, + identity: String, + ) { + with(instance) { + if (MParticle.IdentityType.CustomerId == identityType) { + setCustomerUserId(identity) + } else if (MParticle.IdentityType.Email == identityType) { + setUserEmails(AppsFlyerProperties.EmailsCryptType.NONE, identity) + } + } + } + + override fun logout(): List = emptyList() + + private fun parseToNestedMap(jsonString: String): Map { + val topLevelMap = mutableMapOf() + try { + if (jsonString.isNullOrEmpty()) { + return topLevelMap + } + val jsonObject = JSONObject(jsonString) + + for (key in jsonObject.keys()) { + val value = jsonObject.get(key) + if (value is JSONObject) { + topLevelMap[key] = parseToNestedMap(value.toString()) + } else { + topLevelMap[key] = value + } + } + } catch (e: Exception) { + Logger.error( + e, + "The AppsFlyer kit was unable to parse the user's ConsentState, consent may not be set correctly on the AppsFlyer SDK", + ) + } + return topLevelMap + } + + private fun searchKeyInNestedMap( + map: Map<*, *>, + key: Any, + ): Any? { + if (map.isNullOrEmpty()) { + return null + } + try { + for ((mapKey, mapValue) in map) { + if (mapKey.toString().equals(key.toString(), ignoreCase = true)) { + return mapValue + } + if (mapValue is Map<*, *>) { + val foundValue = searchKeyInNestedMap(mapValue, key) + if (foundValue != null) { + return foundValue + } + } + } + } catch (e: Exception) { + Logger.error( + e, + "The AppsFlyer kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.", + ) + } + return null + } + + override fun onConsentStateUpdated( + consentState: ConsentState, + consentState1: ConsentState, + ) { + setConsent(consentState1) + } + + private fun setConsent(consentState: ConsentState?) { + if (settings[GDPR_APPLIES].isNullOrEmpty()) { + return + } + val appsFlyerGDPRUser: AppsFlyerConsent + if (!settings[GDPR_APPLIES].toBoolean()) { + appsFlyerGDPRUser = AppsFlyerConsent(false, null, null, null) + } else { + var adStorageConsentValue: Boolean? = null + when (settings[DEFAULT_AD_STORAGE_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adStorageConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adStorageConsentValue = false + } + + var adUserDataConsentValue: Boolean? = null + when (settings[DEFAULT_AD_USER_DATA_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adUserDataConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adUserDataConsentValue = false + } + + var adPersonalizationConsentValue: Boolean? = null + when (settings[DEFAULT_AD_PERSONALIZATION_CONSENT]) { + AppsFlyerConsentValues.GRANTED.consentValue -> adPersonalizationConsentValue = true + AppsFlyerConsentValues.DENIED.consentValue -> adPersonalizationConsentValue = false + } + + val clientConsentSettings = parseToNestedMap(consentState.toString()) + + parseConsentMapping(settings[CONSENT_MAPPING]).iterator().forEach { currentConsent -> + + val isConsentAvailable = + searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key) + + if (isConsentAvailable != null) { + val isConsentGranted: Boolean = + JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean + + when (currentConsent.value) { + "ad_storage" -> adStorageConsentValue = isConsentGranted + + "ad_user_data" -> adUserDataConsentValue = isConsentGranted + + "ad_personalization" -> adPersonalizationConsentValue = isConsentGranted + } + } + } + appsFlyerGDPRUser = AppsFlyerConsent(true, adUserDataConsentValue, adPersonalizationConsentValue, adStorageConsentValue) + } + AppsFlyerLib.getInstance().setConsentData(appsFlyerGDPRUser) + } + + private fun parseConsentMapping(json: String?): Map { + if (json.isNullOrEmpty()) { + return emptyMap() + } + val jsonWithFormat = json.replace("\\", "") + + return try { + JSONArray(jsonWithFormat) + .let { jsonArray -> + (0 until jsonArray.length()) + .associate { + val jsonObject = jsonArray.getJSONObject(it) + val map = jsonObject.getString("map") + val value = jsonObject.getString("value") + map to value + } + } + } catch (jse: JSONException) { + Logger.error( + jse, + "The AppsFlyer kit threw an exception while searching for the configured consent purpose mapping in the current user's consent status.", + ) + emptyMap() + } + } + + override fun onConversionDataSuccess(conversionDataN: MutableMap?) { + var conversionData = conversionDataN + val jsonResult = JSONObject() + + if (conversionData == null) { + conversionData = hashMapOf() + } + + conversionData[INSTALL_CONVERSION_RESULT] = "true" + + for ((key, value) in conversionData) { + try { + jsonResult.put(key, value) + } catch (e: JSONException) { + } + } + + val result = + AttributionResult() + .setParameters(jsonResult) + .setServiceProviderId(configuration.kitId) + kitManager.onResult(result) + } + + override fun onConversionDataFail(conversionFailure: String) { + if (!KitUtils.isEmpty(conversionFailure)) { + val error = + AttributionError() + .setMessage(conversionFailure) + .setServiceProviderId(configuration.kitId) + kitManager.onError(error) + } + } + + fun deepLinkListener() = + DeepLinkListener { deepLinkResult -> + val deepLinkObj = deepLinkResult.deepLink + + when (deepLinkResult.status) { + DeepLinkResult.Status.FOUND -> { + try { + deepLinkObj.clickEvent.put(APP_OPEN_ATTRIBUTION_RESULT, true.toString()) + val result = + AttributionResult() + .setParameters(deepLinkObj.clickEvent) + .setServiceProviderId(configuration.kitId) + kitManager.onResult(result) + } catch (e: Exception) { + return@DeepLinkListener + } + } + DeepLinkResult.Status.NOT_FOUND -> { + return@DeepLinkListener + } + else -> { + val dlError = deepLinkResult.error + if (!KitUtils.isEmpty(dlError.toString())) { + val error = + AttributionError() + .setMessage(dlError.toString()) + .setServiceProviderId(configuration.kitId) + kitManager.onError(error) + } + return@DeepLinkListener + } + } + } + + override fun onAppOpenAttribution(attributionDataN: MutableMap?) { + // do nothing, Appsflyer new UDL implementation will use new deep linking method with both + // custom URI and appsflyer's Onelink + } + + override fun onAttributionFailure(attributionFailure: String) { + // do nothing, Appsflyer new UDL implementation will use new deep linking method with both + // custom URI and appsflyer's Onelink + } + + override fun setInstallReferrer(intent: Intent) { + // do nothing, Appsflyer will fetch the install referrer data internally, + // as long as the proper Play Install Referrer dependency is present. + } + + override fun setLocation(location: Location) { + instance.logLocation(context, location.latitude, location.longitude) + } + + override fun onActivityCreated( + activity: Activity, + bundle: Bundle?, + ): List { + instance.start(activity) + return emptyList() + } + + override fun onActivityStarted(activity: Activity): List = emptyList() + + override fun onActivityResumed(activity: Activity): List = emptyList() + + override fun onActivityPaused(activity: Activity): List = emptyList() + + override fun onActivityStopped(activity: Activity): List = emptyList() + + override fun onActivitySaveInstanceState( + activity: Activity, + bundle: Bundle?, + ): List = emptyList() + + override fun onActivityDestroyed(activity: Activity): List = emptyList() + + override fun onSettingsUpdated(settings: Map) { + settings[SHARING_FILTER_FOR_PARTNERS]?.let { applySharingFilterForPartners(it) } + } + + private fun applySharingFilterForPartners(jsonValue: String) { + val partners = parseSharingFilterForPartners(jsonValue) + if (!partners.isNullOrEmpty()) { + instance.setSharingFilterForPartners(*partners.toTypedArray()) + } + } + + private fun parseSharingFilterForPartners(json: String?): List? { + if (json.isNullOrEmpty()) return null + return try { + val jsonWithFormat = json.replace("\\", "") + val array = JSONArray(jsonWithFormat) + List(array.length()) { i -> array.getString(i) } + } catch (e: JSONException) { + Logger.warning( + "AppsFlyer kit: failed to parse sharingFilterForPartners, " + + "consent filter for partners will not be applied. Error: ${e.message}", + ) + null + } + } + + companion object { + const val DEV_KEY = "devKey" + const val APPSFLYERID_INTEGRATION_KEY = "appsflyer_id_integration_setting" + const val NAME = "AppsFlyer" + const val COMMA = "," + + /** + * This key will be present when returning a result from AppsFlyer's onInstallConversionDataLoaded API + */ + const val INSTALL_CONVERSION_RESULT = "MPARTICLE_APPSFLYER_INSTALL_CONVERSION_RESULT" + + /** + * This key will be present when returning a result from AppsFlyer's onAppOpenAttribution API + */ + const val APP_OPEN_ATTRIBUTION_RESULT = + "MPARTICLE_APPSFLYER_APP_OPEN_ATTRIBUTION_RESULT" + + fun generateProductIdList(event: CommerceEvent?): List? = + event?.products?.filter { !KitUtils.isEmpty(it.sku) }?.let { + if (it.isNotEmpty()) { + it.map { it.sku.replace(COMMA, "%2C") } + } else { + null + } + } + + private const val SHARING_FILTER_FOR_PARTNERS = "sharingFilterForPartners" + private const val CONSENT_MAPPING = "consentMapping" + + @Suppress("ktlint:standard:property-naming") + enum class AppsFlyerConsentValues( + val consentValue: String, + ) { + GRANTED("Granted"), + DENIED("Denied"), + } + + const val GDPR_APPLIES = "gdprApplies" + const val DEFAULT_AD_STORAGE_CONSENT = "defaultAdStorageConsent" + const val DEFAULT_AD_USER_DATA_CONSENT = "defaultAdUserDataConsent" + const val DEFAULT_AD_PERSONALIZATION_CONSENT = "defaultAdPersonalizationConsent" + } +} diff --git a/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt new file mode 100644 index 000000000..eb5cf5e49 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerConsent.kt @@ -0,0 +1,8 @@ +package com.appsflyer + +class AppsFlyerConsent( + val isUserSubjectToGDPR: Boolean?, + val hasConsentForDataUsage: Boolean?, + val hasConsentForAdsPersonalization: Boolean?, + val hasConsentForAdStorage: Boolean?, +) diff --git a/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt new file mode 100644 index 000000000..0ce8e82e8 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/appsflyer/AppsFlyerLib.kt @@ -0,0 +1,47 @@ +package com.appsflyer + +import android.content.Context + +class AppsFlyerLib { + private var consentData: AppsFlyerConsent? = null + + fun setConsentData(consent: AppsFlyerConsent) { + consentData = consent + } + + fun getConsentData(): AppsFlyerConsent? = consentData + + fun getConsentState(): MutableMap { + val stateMap = mutableMapOf() + consentData?.let { consent -> + // Use property names directly instead of getter methods + consent.isUserSubjectToGDPR?.let { stateMap["isUserSubjectToGDPR"] = it } + consent.hasConsentForDataUsage?.let { stateMap["hasConsentForDataUsage"] = it } + consent.hasConsentForAdsPersonalization?.let { stateMap["hasConsentForAdsPersonalization"] = it } + consent.hasConsentForAdStorage?.let { stateMap["hasConsentForAdStorage"] = it } + } + return stateMap + } + + companion object { + private var _instance: AppsFlyerLib? = null + + @JvmStatic + fun getInstance(): AppsFlyerLib? { + if (_instance == null) { + _instance = AppsFlyerLib() + } + return _instance + } + + @JvmStatic + fun getInstance(context: Context?): AppsFlyerLib? = getInstance() + + /** + * Access Methods + */ + fun clearInstance() { + _instance = null + } + } +} diff --git a/kits/appsflyer/appsflyer-6/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt new file mode 100644 index 000000000..a38584815 --- /dev/null +++ b/kits/appsflyer/appsflyer-6/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt @@ -0,0 +1,822 @@ +package com.mparticle.kits + +import android.app.Activity +import android.content.Context +import android.net.Uri +import com.appsflyer.AppsFlyerLib +import com.mparticle.MParticle +import com.mparticle.MParticleOptions +import com.mparticle.commerce.CommerceEvent +import com.mparticle.commerce.Product +import com.mparticle.commerce.TransactionAttributes +import com.mparticle.consent.ConsentState +import com.mparticle.consent.GDPRConsent +import com.mparticle.identity.IdentityApi +import com.mparticle.identity.MParticleUser +import com.mparticle.internal.CoreCallbacks +import com.mparticle.internal.CoreCallbacks.KitListener +import junit.framework.Assert.assertEquals +import junit.framework.TestCase +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.mock +import org.mockito.MockitoAnnotations +import java.lang.ref.WeakReference +import java.lang.reflect.Method + +class AppsflyerKitTests { + private var kit = AppsFlyerKit() + private var appsflyer = AppsFlyerLib() + + @Mock + lateinit var user: MParticleUser + + @Before + @Throws(JSONException::class) + fun before() { + AppsFlyerLib.clearInstance() + kit = AppsFlyerKit() + MockitoAnnotations.initMocks(this) + MParticle.setInstance(mock(MParticle::class.java)) + Mockito + .`when`(MParticle.getInstance()?.Identity()) + .thenReturn( + mock( + IdentityApi::class.java, + ), + ) + val kitManager = + KitManagerImpl( + mock( + Context::class.java, + ), + null, + emptyCoreCallbacks, + mock(MParticleOptions::class.java), + ) + kit.kitManager = kitManager + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("id", "-1")) + appsflyer = AppsFlyerLib.getInstance(null)!! + } + + @Test + @Throws(Exception::class) + fun testGetName() { + val name = kit.name + Assert.assertTrue(name.isNotEmpty()) + } + + @Test + @Throws(Exception::class) + fun testParseSharingFilterForPartners_returnsListForValidJson() { + val method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseSharingFilterForPartners", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, """["partner_1", "partner_2"]""") + Assert.assertEquals(listOf("partner_1", "partner_2"), result) + } + + @Test + @Throws(Exception::class) + fun testParseSharingFilterForPartners_returnsNullForEmptyInput() { + val method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseSharingFilterForPartners", + String::class.java, + ) + method.isAccessible = true + Assert.assertNull(method.invoke(kit, "")) + Assert.assertNull(method.invoke(kit, null)) + } + + @Test + @Throws(Exception::class) + fun testParseSharingFilterForPartners_returnsNullForInvalidJson() { + val method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseSharingFilterForPartners", + String::class.java, + ) + method.isAccessible = true + Assert.assertNull(method.invoke(kit, "not a json array")) + } + + @Test + @Throws(Exception::class) + fun testParseSharingFilterForPartners_stripsBackslashes() { + val method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseSharingFilterForPartners", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, """[\"test_1\", \"test_2\"]""") + Assert.assertEquals(listOf("test_1", "test_2"), result) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + @Throws(Exception::class) + fun testOnKitCreate() { + var e: Throwable? = null + try { + val settings = HashMap() + settings["fake setting"] = "fake" + kit.onKitCreate(settings as Map, mock(Context::class.java)) + } catch (ex: Throwable) { + e = ex + } + Assert.assertNotNull(e) + } + + @Test + @Throws(Exception::class) + fun testClassName() { + val options = mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = kit.javaClass.name + for (integration in integrations) { + if (integration.name == className) { + return + } + } + Assert.fail("$className not found as a known integration.") + } + + @Test + @Throws(Exception::class) + fun testGenerateSkuString() { + MParticle.setInstance(mock(MParticle::class.java)) + Mockito + .`when`(MParticle.getInstance()?.environment) + .thenReturn(MParticle.Environment.Production) + Assert.assertNull(AppsFlyerKit.generateProductIdList(null)) + val product = Product.Builder("foo-name", "foo-sku", 50.0).build() + val event = + CommerceEvent + .Builder(Product.PURCHASE, product) + .transactionAttributes(TransactionAttributes("foo")) + .build() + assertEquals(mutableListOf("foo-sku"), AppsFlyerKit.generateProductIdList(event)) + val product2 = Product.Builder("foo-name-2", "foo-sku-2", 50.0).build() + val event2 = + CommerceEvent + .Builder(Product.PURCHASE, product) + .addProduct(product2) + .transactionAttributes(TransactionAttributes("foo")) + .build() + assertEquals( + mutableListOf("foo-sku", "foo-sku-2"), + AppsFlyerKit.generateProductIdList(event2), + ) + val product3 = Product.Builder("foo-name-3", "foo-sku-,3", 50.0).build() + val event3 = + CommerceEvent + .Builder(Product.PURCHASE, product) + .addProduct(product2) + .addProduct(product3) + .transactionAttributes(TransactionAttributes("foo")) + .build() + assertEquals( + mutableListOf("foo-sku", "foo-sku-2", "foo-sku-%2C3"), + AppsFlyerKit.generateProductIdList(event3), + ) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRNotApplied() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "false" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Denied" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = + ConsentState + .builder() + .addGDPRConsentState("Marketing", marketingConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(false, expectedConsentValue) + + val notExpectedConsentKey = + afConsentResults.containsKey("hasConsentForDataUsage") + TestCase.assertEquals(false, notExpectedConsentKey) + + val notExpectedConsentKey2 = + afConsentResults.containsKey("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, notExpectedConsentKey2) + + val notExpectedConsentKey3 = + afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey3) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRAppliedWithoutConsentDefaults() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Unspecified" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Unspecified" + map["defaultAdPersonalizationConsent"] = "Unspecified" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = + ConsentState + .builder() + .addGDPRConsentState("test1", marketingConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val notExpectedConsentKey = + afConsentResults.containsKey("hasConsentForDataUsage") + TestCase.assertEquals(false, notExpectedConsentKey) + + val notExpectedConsentKey2 = + afConsentResults.containsKey("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, notExpectedConsentKey2) + + val notExpectedConsentKey3 = + afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey3) + } + + @Test + @Throws(Exception::class) + fun testConsentWhenGDPRAppliedWithConsentDefaults() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = + ConsentState + .builder() + .addGDPRConsentState("test1", marketingConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults + .getValue("hasConsentForDataUsage") + TestCase.assertEquals(false, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults + .getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults + .getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + } + + @Test + @Throws(Exception::class) + fun testConsentMapping() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val performanceConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val marketingConsent = + GDPRConsent + .builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val testConsent = + GDPRConsent + .builder(false) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = + ConsentState + .builder() + .addGDPRConsentState("Performance", performanceConsent) + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("testconsent", testConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults + .getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults + .getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults + .getValue("hasConsentForAdStorage") + TestCase.assertEquals(false, expectedConsentValue4) + } + + @Test + fun onConsentStateUpdatedTestPerformance_And_Marketing_are_true() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Granted" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val performanceConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val marketingConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = + ConsentState + .builder() + .addGDPRConsentState("Performance", performanceConsent) + .addGDPRConsentState("Marketing", marketingConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults + .getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults + .getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults + .getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + } + + @Test + fun onConsentStateUpdatedTest_When_No_Defaults_Values() { + val map = HashMap() + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = + GDPRConsent + .builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = + ConsentState + .builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + kit.onConsentStateUpdated(state, state) + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults + .getValue("hasConsentForDataUsage") + TestCase.assertEquals(true, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults + .getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(true, expectedConsentValue3) + + val notExpectedConsentKey = + afConsentResults.containsKey("hasConsentForAdStorage") + TestCase.assertEquals(false, notExpectedConsentKey) + } + + @Test + fun onConsentStateUpdatedTest_When_No_DATA_From_Server() { + val marketingConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = + GDPRConsent + .builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = + ConsentState + .builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + kit.onConsentStateUpdated(state, state) + + TestCase.assertEquals(0, appsflyer.getConsentState().size) + } + + @Test + fun onConsentStateUpdatedTest_No_consentMappingSDK() { + val map = HashMap() + map["gdprApplies"] = "true" + map["defaultAdStorageConsent"] = "Granted" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Denied" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = + GDPRConsent + .builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val state = + ConsentState + .builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + kit.onConsentStateUpdated(state, state) + + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(true, expectedConsentValue) + + val expectedConsentValue2 = + afConsentResults + .getValue("hasConsentForDataUsage") + TestCase.assertEquals(false, expectedConsentValue2) + + val expectedConsentValue3 = + afConsentResults + .getValue("hasConsentForAdsPersonalization") + TestCase.assertEquals(false, expectedConsentValue3) + + val expectedConsentValue4 = + afConsentResults + .getValue("hasConsentForAdStorage") + TestCase.assertEquals(true, expectedConsentValue4) + } + + @Test + fun onConsentStateUpdatedTest_When_default_is_Unspecified_And_No_consentMappingSDK_And_GDPR_Not_Applied() { + val map = HashMap() + map["gdprApplies"] = "false" + map["defaultAdStorageConsent"] = "Unspecified" + map["defaultAdUserDataConsent"] = "Unspecified" + map["defaultAdPersonalizationConsent"] = "Unspecified" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", JSONObject(map as Map<*, *>))) + + val marketingConsent = + GDPRConsent + .builder(true) + .document("Test consent") + .location("17 Cherry Tree Lane") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + + val performanceConsent = + GDPRConsent + .builder(true) + .document("parental_consent_agreement_v2") + .location("17 Cherry Tree Lan 3") + .hardwareId("IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702") + .build() + val state = + ConsentState + .builder() + .addGDPRConsentState("Marketing", marketingConsent) + .addGDPRConsentState("Performance", performanceConsent) + .build() + kit.onConsentStateUpdated(state, state) + + TestCase.assertEquals(1, appsflyer.getConsentState().size) + val afConsentResults = appsflyer.getConsentState() + val expectedConsentValue = + afConsentResults + .getValue("isUserSubjectToGDPR") + TestCase.assertEquals(false, expectedConsentValue) + } + + @Test + fun testParseToNestedMap_When_JSON_Is_INVALID() { + var jsonInput = + "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}','performance':'{'consented':true,'timestamp':1711038269644,'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'},'CCPA':'{'consented':true,'timestamp':1711038269644,'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}" + + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseToNestedMap", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(mutableMapOf(), result) + } + + @Test + fun testParseToNestedMap_When_JSON_Is_Empty() { + var jsonInput = "" + + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseToNestedMap", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(mutableMapOf(), result) + } + + @Test + fun testSearchKeyInNestedMap_When_Input_Key_Is_Empty_String() { + val map = + mapOf( + "GDPR" to true, + "marketing" to + mapOf( + "consented" to false, + "document" to + mapOf( + "timestamp" to 1711038269644, + ), + ), + ) + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "searchKeyInNestedMap", + Map::class.java, + Any::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, map, "") + Assert.assertEquals(null, result) + } + + @Test + fun testSearchKeyInNestedMap_When_Input_Is_Empty_Map() { + val emptyMap: Map = emptyMap() + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "searchKeyInNestedMap", + Map::class.java, + Any::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, emptyMap, "1") + Assert.assertEquals(null, result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_Empty_Json() { + val emptyJson = "" + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, emptyJson) + Assert.assertEquals(emptyMap(), result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_Invalid_Json() { + var jsonInput = + "{'GDPR':{'marketing':'{:false,'timestamp':1711038269644:'Test consent','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}','performance':'{'consented':true,'timestamp':1711038269644,'document':'parental_consent_agreement_v2','location':'17 Cherry Tree Lan 3','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'},'CCPA':'{'consented':true,'timestamp':1711038269644,'document':'ccpa_consent_agreement_v3','location':'17 Cherry Tree Lane','hardware_id':'IDFA:a5d934n0-232f-4afc-2e9a-3832d95zc702'}'}" + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, jsonInput) + Assert.assertEquals(emptyMap(), result) + } + + @Test + fun testParseConsentMapping_When_Input_Is_NULL() { + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "parseConsentMapping", + String::class.java, + ) + method.isAccessible = true + val result = method.invoke(kit, null) + Assert.assertEquals(emptyMap(), result) + } + + private var emptyCoreCallbacks: CoreCallbacks = + object : CoreCallbacks { + var activity = Activity() + + override fun isBackgrounded(): Boolean = false + + override fun getUserBucket(): Int = 0 + + override fun isEnabled(): Boolean = false + + override fun setIntegrationAttributes( + i: Int, + map: Map, + ) {} + + override fun getIntegrationAttributes(i: Int): Map? = null + + override fun getCurrentActivity(): WeakReference = WeakReference(activity) + + override fun getLatestKitConfiguration(): JSONArray? = null + + override fun getDataplanOptions(): MParticleOptions.DataplanOptions? = null + + override fun isPushEnabled(): Boolean = false + + override fun getPushSenderId(): String? = null + + override fun getPushInstanceId(): String? = null + + override fun getLaunchUri(): Uri? = null + + override fun getLaunchAction(): String? = null + + override fun getKitListener(): KitListener = + object : KitListener { + override fun kitFound(kitId: Int) {} + + override fun kitConfigReceived( + kitId: Int, + configuration: String?, + ) {} + + override fun kitExcluded( + kitId: Int, + reason: String?, + ) {} + + override fun kitStarted(kitId: Int) {} + + override fun onKitApiCalled( + kitId: Int, + used: Boolean?, + vararg objects: Any?, + ) {} + + override fun onKitApiCalled( + methodName: String?, + kitId: Int, + used: Boolean?, + vararg objects: Any?, + ) { + } + } + } +} diff --git a/kits/apptentive-kit b/kits/apptentive-kit deleted file mode 160000 index 658cb9112..000000000 --- a/kits/apptentive-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 658cb9112df0bde4c219c5057441ffc8a6308a79 diff --git a/kits/apptentive/apptentive-6/README.md b/kits/apptentive/apptentive-6/README.md new file mode 100644 index 000000000..8196b539f --- /dev/null +++ b/kits/apptentive/apptentive-6/README.md @@ -0,0 +1,32 @@ +# Apptentive Kit Integration + +This repository contains the [Apptentive](https://www.apptentive.com/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-apptentive-kit:5+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Apptentive detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. +4. Reference Apptentive's integration doc below for more features. +5. Check out the example app below. + +## Documentation + +[Apptentive integration](https://docs.mparticle.com/integrations/apptentive/event/) + +[Apptentive-mParticle Integration](https://learn.apptentive.com/knowledge-base/mparticle-integration-android) + +## Example App + +[Apptentive Android mParticle Example](https://github.com/apptentive/android-mparticle-example) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/apptentive/apptentive-6/build.gradle b/kits/apptentive/apptentive-6/build.gradle new file mode 100644 index 000000000..0648c4cb7 --- /dev/null +++ b/kits/apptentive/apptentive-6/build.gradle @@ -0,0 +1,69 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-apptentive" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.apptentive' + buildFeatures { + buildConfig = true + } + defaultConfig { + minSdkVersion 21 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +dependencies { + api 'com.apptentive:apptentive-kit-android:[6.9.1,7.0.0)' + testImplementation 'io.mockk:mockk:1.13.3' +} diff --git a/kits/apptentive/apptentive-6/consumer-proguard.pro b/kits/apptentive/apptentive-6/consumer-proguard.pro new file mode 100644 index 000000000..e29dec0db --- /dev/null +++ b/kits/apptentive/apptentive-6/consumer-proguard.pro @@ -0,0 +1,19 @@ +# include in this file rules Apptentive want applied to a +# consumer of this library when the consumer proguards itself. + +# Marshmallow removed Notification.setLatestEventInfo() +-dontwarn android.app.Notification +-dontwarn android.support.v7.** +-keep class android.support.v7.** { *; } +-keep interface android.support.v7.** { *; } + +-dontwarn android.support.v4.app.** +-keep class android.support.v4.app.** { *; } +-keep interface android.support.v4.app.** { *; } + +-dontwarn com.google.android.gms.** +-keep public class com.google.android.gms.** + +-dontwarn com.apptentive.android.sdk.** +-keepattributes SourceFile,LineNumberTable +-keep class com.apptentive.android.sdk.** { *; } \ No newline at end of file diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/build.gradle b/kits/apptentive/apptentive-6/example/example-kotlin/build.gradle new file mode 100644 index 000000000..21ae7f517 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/build.gradle @@ -0,0 +1,38 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.apptentive.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.apptentive.example.kotlin' + minSdk 21 + targetSdk 35 + versionCode 1 + versionName '1.0' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation project(':kits:apptentive:apptentive-6') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' +} diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/AndroidManifest.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/ExampleApplication.kt b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..893e14047 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.apptentive.example.kotlin + +import android.app.Application +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : Application() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/MainActivity.kt b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..b9822e943 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptentive/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.apptentive.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/colors.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/strings.xml b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..239b17ffa --- /dev/null +++ b/kits/apptentive/apptentive-6/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Apptentive Kit Kotlin Example + diff --git a/kits/apptentive/apptentive-6/gradle.properties b/kits/apptentive/apptentive-6/gradle.properties new file mode 100644 index 000000000..edb1202c3 --- /dev/null +++ b/kits/apptentive/apptentive-6/gradle.properties @@ -0,0 +1,4 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2560m \ No newline at end of file diff --git a/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.jar b/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.properties b/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/apptentive/apptentive-6/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/apptentive/apptentive-6/gradlew b/kits/apptentive/apptentive-6/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/apptentive/apptentive-6/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/apptentive/apptentive-6/gradlew.bat b/kits/apptentive/apptentive-6/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/apptentive/apptentive-6/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/apptentive/apptentive-6/libs/testutils.aar b/kits/apptentive/apptentive-6/libs/testutils.aar new file mode 100644 index 000000000..cf49b4189 Binary files /dev/null and b/kits/apptentive/apptentive-6/libs/testutils.aar differ diff --git a/kits/apptentive/apptentive-6/settings.gradle.kts b/kits/apptentive/apptentive-6/settings.gradle.kts new file mode 100644 index 000000000..09e637bfe --- /dev/null +++ b/kits/apptentive/apptentive-6/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-apptentive-kit" +include(":") diff --git a/kits/apptentive/apptentive-6/src/main/AndroidManifest.xml b/kits/apptentive/apptentive-6/src/main/AndroidManifest.xml new file mode 100644 index 000000000..61363294b --- /dev/null +++ b/kits/apptentive/apptentive-6/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKit.kt b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKit.kt new file mode 100644 index 000000000..e0e46217c --- /dev/null +++ b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKit.kt @@ -0,0 +1,366 @@ +package com.mparticle.kits + +import android.app.Application +import android.content.Context +import apptentive.com.android.feedback.Apptentive +import apptentive.com.android.feedback.ApptentiveConfiguration +import apptentive.com.android.feedback.RegisterResult +import apptentive.com.android.util.InternalUseOnly +import apptentive.com.android.util.LogLevel +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticle.IdentityType +import com.mparticle.consent.ConsentState +import com.mparticle.identity.MParticleUser +import com.mparticle.internal.Logger +import com.mparticle.kits.KitIntegration.IdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener +import java.util.LinkedList +import java.util.concurrent.TimeUnit +import kotlin.collections.HashMap + +@OptIn(InternalUseOnly::class) +class ApptentiveKit : + KitIntegration(), + KitIntegration.EventListener, + IdentityListener, + UserAttributeListener { + private var enableTypeDetection = true + private var lastKnownFirstName: String? = null + private var lastKnownLastName: String? = null + + //region KitIntegration + override fun getName(): String = NAME + + override fun onKitCreate( + settings: Map, + context: Context, + ): List { + val apptentiveAppKey = settings[APPTENTIVE_APP_KEY] + val apptentiveAppSignature = settings[APPTENTIVE_APP_SIGNATURE] + require(!KitUtils.isEmpty(apptentiveAppKey)) { KEY_REQUIRED } + require(!KitUtils.isEmpty(apptentiveAppSignature)) { SIGNATURE_REQUIRED } + enableTypeDetection = StringUtils.tryParseSettingFlag(settings, ENABLE_TYPE_DETECTION, true) + + if (apptentiveAppKey != null && apptentiveAppSignature != null) { + val configuration = ApptentiveConfiguration(apptentiveAppKey, apptentiveAppSignature) + configuration.logLevel = getApptentiveLogLevel() + configuration.distributionVersion = com.mparticle.BuildConfig.VERSION_NAME + configuration.distributionName = "mParticle" + configuration.shouldSanitizeLogMessages = + StringUtils.tryParseSettingFlag(settings, SHOULD_SANITIZE_LOG_MESSAGES, true) + configuration.shouldEncryptStorage = + StringUtils.tryParseSettingFlag(settings, SHOULD_ENCRYPT_STORAGE, false) + configuration.shouldInheritAppTheme = + StringUtils.tryParseSettingFlag(settings, SHOULD_INHERIT_APP_THEME, true) + configuration.customAppStoreURL = settings[CUSTOM_APP_STORE_URL] + configuration.ratingInteractionThrottleLength = + StringUtils.tryParseLongSettingFlag( + settings, + RATING_INTERACTION_THROTTLE_LENGTH, + TimeUnit.DAYS.toMillis(7), + ) + Apptentive.register( + context.applicationContext as Application, + configuration, + ) { registerResult -> + if (registerResult is RegisterResult.Success) { + Apptentive.setMParticleId(currentUser?.id.toString()) + } + } + } + return emptyList() + } + + override fun setOptOut(optedOut: Boolean): List = emptyList() + + override fun supportsAttributeLists(): Boolean = false + + override fun onConsentStateUpdated( + oldState: ConsentState?, + newState: ConsentState?, + ) { + // Ignored + } + + //endregion + + //region UserAttributeListener + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + // Ignored + } + + override fun onRemoveUserAttribute( + key: String?, + ) { + key?.let { + Apptentive.removeCustomPersonData(it) + } + } + + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + if (key != null && value != null) { + when (key.lowercase()) { + MParticle.UserAttributes.FIRSTNAME.lowercase() -> { + lastKnownFirstName = value.toString() + } + MParticle.UserAttributes.LASTNAME.lowercase() -> { + lastKnownLastName = value.toString() + } + else -> { + addCustomPersonData(key, value.toString()) + return + } + } + val fullName = + listOfNotNull(lastKnownFirstName, lastKnownLastName) + .joinToString(separator = " ") + .trim() + if (fullName.isNotBlank()) { + Logger.debug("Setting user name $fullName") + Apptentive.setPersonName(fullName) + } + } + } + + override fun onSetUserTag( + key: String?, + ) { + // Ignored + } + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, + ) { + // Ignored + } + + override fun onSetAllUserAttributes( + userAttributes: MutableMap?, + userAttributeLists: MutableMap>?, + ) { + userAttributes?.let { userAttribute -> + val firstName = userAttribute[MParticle.UserAttributes.FIRSTNAME] ?: "" + val lastName = userAttribute[MParticle.UserAttributes.LASTNAME] ?: "" + val fullName = + listOfNotNull(firstName, lastName) + .joinToString(separator = " ") + .trim() + if (fullName.isNotBlank()) { + Logger.debug("Setting user name $fullName") + Apptentive.setPersonName(fullName) + } + userAttribute + .filterKeys { key -> + key != MParticle.UserAttributes.FIRSTNAME && key != MParticle.UserAttributes.LASTNAME + }.map { + addCustomPersonData(it.key, it.value) + } + } + } + + //endregion + + //region EventListener + override fun leaveBreadcrumb(breadcrumb: String): List = emptyList() + + override fun logError( + message: String, + errorAttributes: Map, + ): List = emptyList() + + override fun logException( + exception: Exception, + exceptionAttributes: Map, + message: String, + ): List = emptyList() + + override fun logEvent(event: MPEvent): List { + engage(event.eventName, event.customAttributeStrings) + val messageList = LinkedList() + messageList.add(ReportingMessage.fromEvent(this, event)) + return messageList + } + + override fun logScreen( + screenName: String, + eventAttributes: Map, + ): List { + engage(screenName, eventAttributes) + val messages = LinkedList() + messages.add( + ReportingMessage( + this, + ReportingMessage.MessageType.SCREEN_VIEW, + System.currentTimeMillis(), + eventAttributes, + ), + ) + return messages + } //endregion + + //region IdentityListener + override fun onIdentifyCompleted( + mParticleUser: MParticleUser?, + identityApiRequest: FilteredIdentityApiRequest?, + ) { + setUserIdentity(mParticleUser) + } + + override fun onLoginCompleted( + mParticleUser: MParticleUser?, + identityApiRequest: FilteredIdentityApiRequest?, + ) { + setUserIdentity(mParticleUser) + } + + override fun onLogoutCompleted( + mParticleUser: MParticleUser?, + identityApiRequest: FilteredIdentityApiRequest?, + ) { + setUserIdentity(mParticleUser) + } + + override fun onModifyCompleted( + mParticleUser: MParticleUser?, + identityApiRequest: FilteredIdentityApiRequest?, + ) { + setUserIdentity(mParticleUser) + } + + override fun onUserIdentified(mParticleUser: MParticleUser?) { + // Ignored + } + + //endregion + + //region Helpers + private fun getApptentiveLogLevel(): LogLevel = + when (Logger.getMinLogLevel()) { + MParticle.LogLevel.VERBOSE -> LogLevel.Verbose + MParticle.LogLevel.DEBUG -> LogLevel.Debug + MParticle.LogLevel.INFO -> LogLevel.Info + MParticle.LogLevel.WARNING -> LogLevel.Warning + MParticle.LogLevel.ERROR -> LogLevel.Error + MParticle.LogLevel.NONE -> LogLevel.Info + null -> LogLevel.Info + } + + private fun setUserIdentity(user: MParticleUser?) { + user?.userIdentities?.entries?.let { + for (i in it.indices) { + val entry = it.elementAt(i) + when (entry.key) { + IdentityType.CustomerId -> { + if (KitUtils.isEmpty(Apptentive.getPersonName())) { + // Use id as customer name if no full name is set yet. + Logger.debug("Setting customer id as user name ${entry.value}") + Apptentive.setPersonName(entry.value) + } + } + IdentityType.Email -> { + Logger.debug("Setting customer email ${entry.value}") + Apptentive.setPersonEmail(entry.value) + } + else -> Logger.debug("Other identity type") + } + } + } + } + + private fun engage( + event: String, + customData: Map?, + ) { + Apptentive.engage(event, parseCustomData(customData)) + } + + // Apptentive SDK does not provide a function which accepts Object as custom data so we need to cast + private fun addCustomPersonData( + key: String, + value: String, + ) { + // original key + Logger.debug("Adding custom person data $key to $value") + Apptentive.addCustomPersonData(key, value) + + // typed key + if (enableTypeDetection) { + when (val typedValue = CustomDataParser.parseValue(value)) { + is String -> { + // the value is already set + } + is Boolean -> { + Apptentive.addCustomPersonData(key + SUFFIX_KEY_FLAG, typedValue) + } + is Number -> { + Apptentive.addCustomPersonData(key + SUFFIX_KEY_NUMBER, typedValue) + } + else -> { + Logger.error( + "Unexpected custom person data type:${typedValue?.javaClass}", + ) + } + } + } + } + + private fun parseCustomData(map: Map?): Map? { + if (map != null) { + val res: MutableMap = HashMap() + for ((key, value) in map) { + // original key + res[key] = value + + // typed key + if (enableTypeDetection) { + when (val typedValue = CustomDataParser.parseValue(value)) { + is String -> { + // the value is already set + } + is Boolean -> { + res[key + SUFFIX_KEY_FLAG] = typedValue + } + is Number -> { + res[key + SUFFIX_KEY_NUMBER] = typedValue + } + else -> { + Logger.error( + "Unexpected custom data type:${typedValue?.javaClass}", + ) + } + } + } + } + return res + } + return null + } + //endregion + + companion object { + private const val APPTENTIVE_APP_KEY = "apptentiveAppKey" + private const val APPTENTIVE_APP_SIGNATURE = "apptentiveAppSignature" + private const val ENABLE_TYPE_DETECTION = "enableTypeDetection" + private const val SHOULD_INHERIT_APP_THEME = "shouldInheritAppTheme" + private const val SHOULD_SANITIZE_LOG_MESSAGES = "shouldSanitizeLogMessages" + private const val SHOULD_ENCRYPT_STORAGE = "shouldEncryptStorage" + private const val RATING_INTERACTION_THROTTLE_LENGTH = "ratingInteractionThrottleLength" + private const val CUSTOM_APP_STORE_URL = "customAppStoreURL" + private const val SUFFIX_KEY_FLAG = "_flag" + private const val SUFFIX_KEY_NUMBER = "_number" + private const val KEY_REQUIRED = + "Apptentive App Key is required. If you are migrating from a previous version, you may need to enter the new Apptentive App Key and Signature on the mParticle website." + private const val SIGNATURE_REQUIRED = + "Apptentive App Signature is required. If you are migrating from a previous version, you may need to enter the new Apptentive App Key and Signature on the mParticle website." + const val NAME = "Apptentive" + } +} diff --git a/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKitUtils.kt b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKitUtils.kt new file mode 100644 index 000000000..ecf7c818f --- /dev/null +++ b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/ApptentiveKitUtils.kt @@ -0,0 +1,47 @@ +package com.mparticle.kits + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Context.RECEIVER_EXPORTED +import android.content.Intent +import android.content.IntentFilter +import android.os.Build +import apptentive.com.android.feedback.Apptentive +import apptentive.com.android.feedback.ApptentiveActivityInfo +import com.mparticle.MParticle +import com.mparticle.internal.Logger + +object ApptentiveKitUtils { + @JvmStatic + fun registerApptentiveActivityContext(callback: ApptentiveActivityInfo) { + val broadcastReceiver = + object : BroadcastReceiver() { + override fun onReceive( + context: Context, + intent: Intent, + ) { + if (intent.action?.startsWith(MParticle.ServiceProviders.BROADCAST_ACTIVE) == true) { + Apptentive.registerApptentiveActivityInfoCallback(callback) + } + } + } + if (MParticle.getInstance()?.isKitActive(MParticle.ServiceProviders.APPTENTIVE) == true) { + Apptentive.registerApptentiveActivityInfoCallback(callback) + Logger.debug("ApptentiveKitUtils", "registerApptentiveActivityContext: kit is active") + } else { + val filter = + IntentFilter(MParticle.ServiceProviders.BROADCAST_ACTIVE + MParticle.ServiceProviders.APPTENTIVE) + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { + callback + .getApptentiveActivityInfo() + ?.registerReceiver(broadcastReceiver, filter, RECEIVER_EXPORTED) + Logger.debug("ApptentiveKitUtils", "registerApptentiveActivityContext: SDK 33+") + } else { + callback + .getApptentiveActivityInfo() + ?.registerReceiver(broadcastReceiver, filter) + Logger.debug("ApptentiveKitUtils", "registerApptentiveActivityContext: SDK < 33") + } + } + } +} diff --git a/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/CustomDataParser.kt b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/CustomDataParser.kt new file mode 100644 index 000000000..4926752d0 --- /dev/null +++ b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/CustomDataParser.kt @@ -0,0 +1,39 @@ +package com.mparticle.kits + +import apptentive.com.android.util.InternalUseOnly +import apptentive.com.android.util.Log +import apptentive.com.android.util.LogTag +import kotlin.Any +import kotlin.Exception +import kotlin.String + +@OptIn(InternalUseOnly::class) +internal object CustomDataParser { + fun parseCustomData(map: Map): Map { + val res = HashMap() + for ((key, value) in map) { + res[key] = parseValue(value) + } + return res + } + + @JvmStatic + fun parseValue(value: String?): Any? = + try { + if (value != null) parseValueGuarded(value) else null + } catch (e: Exception) { + Log.e(LogTag("pParticle"), "Unable to parse custom data value") + value + } + + private fun parseValueGuarded(value: String): Any { + // check for boolean + if ("true".equals(value, true) || "false".equals(value, true)) { + return value.toBoolean() + } + + // check for number + val number = StringUtils.tryParseNumber(value) + return number ?: value + } +} diff --git a/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/StringUtils.kt b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/StringUtils.kt new file mode 100644 index 000000000..11c8dbd7d --- /dev/null +++ b/kits/apptentive/apptentive-6/src/main/kotlin/com/mparticle/kits/StringUtils.kt @@ -0,0 +1,59 @@ +package com.mparticle.kits + +internal object StringUtils { + @JvmStatic + fun tryParseSettingFlag( + settings: Map, + key: String?, + defaultValue: Boolean, + ): Boolean { + val value = settings[key] + return value?.toBoolean() ?: defaultValue + } + + fun tryParseLongSettingFlag( + settings: Map, + key: String?, + defaultValue: Long, + ): Long { + val value = + key?.let { + settings[key] + } + return try { + value?.toLong() ?: defaultValue + } catch (e: NumberFormatException) { + defaultValue + } + } + + @JvmStatic + fun tryParseNumber(value: String): Number? { + val longValue = tryParseLong(value) + return if (longValue != null) { + if (isInIntegerRange(longValue)) { + longValue.toInt() + } else { + longValue + } + } else { + tryParseDouble(value) + } + } + + private fun isInIntegerRange(value: Long): Boolean = value >= Int.MIN_VALUE && value <= Int.MAX_VALUE + + private fun tryParseLong(value: String): Long? = + try { + value.toLong() + } catch (e: NumberFormatException) { + null + } + + private fun tryParseDouble(value: String): Double? = + try { + value.toDouble() + } catch (e: NumberFormatException) { + null + } +} diff --git a/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/ApptentiveKitTest.kt b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/ApptentiveKitTest.kt new file mode 100644 index 000000000..64a70cc07 --- /dev/null +++ b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/ApptentiveKitTest.kt @@ -0,0 +1,162 @@ +package com.mparticle.kits + +import android.content.Context +import apptentive.com.android.feedback.Apptentive +import apptentive.com.android.util.InternalUseOnly +import com.mparticle.MParticle +import com.mparticle.MParticleOptions +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito + +class ApptentiveKitTest { + private val kit: ApptentiveKit + get() = ApptentiveKit() + + @Test + @Throws(Exception::class) + fun testGetName() { + val name = kit.name + Assert.assertTrue(!name.isNullOrEmpty()) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + @Throws(Exception::class) + fun testOnKitCreate() { + var e: Exception? = null + try { + val kit = kit + val settings = HashMap() + settings["fake setting"] = "fake" + (kit as KitIntegration).onKitCreate(settings, Mockito.mock(Context::class.java)) + } catch (ex: Exception) { + e = ex + } + Assert.assertNotNull(e) + } + + @Test + @Throws(Exception::class) + fun testClassName() { + val options = Mockito.mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = kit.javaClass.name + for (integration in integrations) { + if (integration.name == className) { + return + } + } + Assert.fail("$className not found as a known integration.") + } + + @Test + fun testLastNameValueOnSetUserAttribute() { + mockkStatic(Apptentive::class) + every { Apptentive.setPersonName(any()) } returns Unit + val key = MParticle.UserAttributes.LASTNAME + val value = "Doe" + + // when + kit.onSetUserAttribute(key, value) + + // then + verify { Apptentive.setPersonName("Doe") } + } + + @OptIn(InternalUseOnly::class) + @Test + fun testFirstNameValueOnSetUserAttribute() { + mockkStatic(Apptentive::class) + every { Apptentive.setPersonName(any()) } returns Unit + + val key = MParticle.UserAttributes.FIRSTNAME + val value = "John" + + // when + kit.onSetUserAttribute(key, value) + + // then + verify { Apptentive.setPersonName("John") } + } + + @Test + fun testPersonCustomData() { + mockkStatic(Apptentive::class) + every { Apptentive.addCustomPersonData(any(), any()) } returns Unit + every { Apptentive.addCustomPersonData(any(), 30) } returns Unit + val key = "age" + val value = "30" + + // when + kit.onSetUserAttribute(key, value) + + // then + // enableTypeDetection is false & 30 will be passed as string + verify { Apptentive.addCustomPersonData("age", "30") } + } + + @Test + fun testOnSetUserAttributeWithNullKey() { + mockkStatic(Apptentive::class) + + val key = null + val value = "30" + + // when + kit.onSetUserAttribute(key, value) + + // then + verify(exactly = 0) { Apptentive.setPersonName(any()) } + verify(exactly = 0) { Apptentive.addCustomPersonData(any(), any()) } + verify(exactly = 0) { Apptentive.addCustomPersonData(any(), any()) } + verify(exactly = 0) { Apptentive.addCustomPersonData(any(), any()) } + } + + // onSetAllUserAttributes test + + @Test + fun testFullNameInTheUserAttributes() { + val userAttributes = + mutableMapOf( + MParticle.UserAttributes.FIRSTNAME to "John", + MParticle.UserAttributes.LASTNAME to "Doe", + ) + + mockkStatic(Apptentive::class) + every { Apptentive.setPersonName(any()) } returns Unit + + // when + kit.onSetAllUserAttributes(userAttributes, null) + + verify { + Apptentive.setPersonName("John Doe") + } + } + + @Test + fun testListOfCustomPersonData() { + val userAttributes = + mutableMapOf( + "key1" to "value1", + "key2" to "20", + ) + mockkStatic(Apptentive::class) + every { Apptentive.addCustomPersonData(any(), any()) } returns Unit + every { Apptentive.addCustomPersonData(any(), 20) } returns Unit + kit.onSetAllUserAttributes(userAttributes, null) + + verify { + Apptentive.addCustomPersonData("key1", "value1") + Apptentive.addCustomPersonData("key2", "20") + } + } +} diff --git a/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/CustomDataParserTest.kt b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/CustomDataParserTest.kt new file mode 100644 index 000000000..8f720ee76 --- /dev/null +++ b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/CustomDataParserTest.kt @@ -0,0 +1,33 @@ +package com.mparticle.kits + +import com.mparticle.kits.CustomDataParser.parseValue +import org.junit.Assert +import org.junit.Test + +class CustomDataParserTest { + @Test + fun testParseData() { + // boolean + Assert.assertEquals(true, parseValue("true")) + Assert.assertEquals(true, parseValue("True")) + Assert.assertEquals(false, parseValue("false")) + Assert.assertEquals(false, parseValue("False")) + + // integer + Assert.assertEquals(12345, parseValue("12345")) + Assert.assertEquals(-12345, parseValue("-12345")) + Assert.assertEquals(Int.MIN_VALUE, parseValue(Int.MIN_VALUE.toString())) + Assert.assertEquals(Int.MAX_VALUE, parseValue(Int.MAX_VALUE.toString())) + + // long + Assert.assertEquals(Long.MIN_VALUE, parseValue(Long.MIN_VALUE.toString())) + Assert.assertEquals(Long.MAX_VALUE, parseValue(Long.MAX_VALUE.toString())) + + // double + Assert.assertEquals(3.14, parseValue("3.14")) + Assert.assertEquals(-3.14, parseValue("-3.14")) + + // string + Assert.assertEquals("test", parseValue("test")) + } +} diff --git a/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/StringUtilsTest.kt b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/StringUtilsTest.kt new file mode 100644 index 000000000..565906c1d --- /dev/null +++ b/kits/apptentive/apptentive-6/src/test/kotlin/com/mparticle/kits/StringUtilsTest.kt @@ -0,0 +1,68 @@ +package com.mparticle.kits + +import com.mparticle.kits.StringUtils.tryParseLongSettingFlag +import com.mparticle.kits.StringUtils.tryParseNumber +import com.mparticle.kits.StringUtils.tryParseSettingFlag +import org.junit.Assert +import org.junit.Test +import java.util.HashMap +import java.util.concurrent.TimeUnit + +class StringUtilsTest { + @Test + fun testParseNumber() { + Assert.assertEquals(Integer.valueOf(12345), tryParseNumber("12345") as Int?) + Assert.assertEquals( + java.lang.Long.valueOf(21474836479L), + tryParseNumber("21474836479") as Long?, + ) + Assert.assertEquals( + java.lang.Long.valueOf(-21474836489L), + tryParseNumber("-21474836489") as Long?, + ) + Assert.assertEquals(java.lang.Double.valueOf(12345.0), tryParseNumber("12345.0") as Double?) + Assert.assertNull(tryParseNumber("test")) + } + + @Test + fun testSettingsFlag() { + val data = HashMap() + data["key"] = "true" + Assert.assertTrue(tryParseSettingFlag(data, "key", false)) + } + + @Test + fun testLongSettingFlag() { + // key is not available + var data = mapOf() + val default = TimeUnit.DAYS.toMillis(7) + Assert.assertEquals(default, tryParseLongSettingFlag(data, "key", default)) + + // key with a long value + data = mapOf("key" to "500") + Assert.assertEquals(500L, tryParseLongSettingFlag(data, "key", default)) + + // key with a invalid type + data = mapOf("key" to "value") + Assert.assertEquals(default, tryParseLongSettingFlag(data, "key", default)) + + // key that is not available in the map + Assert.assertEquals(default, tryParseLongSettingFlag(data, "key_1", default)) + + // null key + Assert.assertEquals(default, tryParseLongSettingFlag(data, "null", default)) + } + + @Test + fun testMissingSettingsFlag() { + val data = HashMap() + Assert.assertFalse(tryParseSettingFlag(data, "key", false)) + } + + @Test + fun testMissingCorruptedSettingsFlag() { + val data = HashMap() + data["key"] = "123" + Assert.assertFalse(tryParseSettingFlag(data, "key", false)) + } +} diff --git a/kits/apptimize-kit b/kits/apptimize-kit deleted file mode 160000 index cf998297b..000000000 --- a/kits/apptimize-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cf998297bd50c59c8cba3af08f2a4b02200acde2 diff --git a/kits/apptimize/apptimize-3/README.md b/kits/apptimize/apptimize-3/README.md new file mode 100644 index 000000000..39afc23f5 --- /dev/null +++ b/kits/apptimize/apptimize-3/README.md @@ -0,0 +1,34 @@ +# Apptimize Kit Integration + +This repository contains the [Apptimize](https://www.apptimize.com/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. The Apptimize Kit requires that you add Apptimize's Maven server to your buildscript: + + ```groovy + repositories { + maven { + url 'https://maven.apptimize.com/artifactory/repo' + } + } + ``` + +2. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-apptimize-kit:5+' + } + ``` + +3. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Apptimize detected"` in the output of `adb logcat`. +4. Reference mParticle's integration docs below to enable the integration. + +## Documentation + +[Apptimize integration](https://docs.mparticle.com/integrations/apptimize) + +## License + +[Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/apptimize/apptimize-3/build.gradle b/kits/apptimize/apptimize-3/build.gradle new file mode 100644 index 000000000..7d6b9410d --- /dev/null +++ b/kits/apptimize/apptimize-3/build.gradle @@ -0,0 +1,66 @@ +buildscript { + ext.kotlin_version = '2.1.20' + if (!project.hasProperty('version') || project.version.equals('unspecified')) { + project.version = '+' + } + + repositories { + google() + mavenLocal() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.mparticle:android-kit-plugin:' + project.version + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +plugins { + id "org.sonarqube" version "3.5.0.2730" + id "org.jlleitschuh.gradle.ktlint" version "13.0.0" +} + +sonarqube { + properties { + property "sonar.projectKey", "mparticle-android-integration-apptimize" + property "sonar.organization", "mparticle" + property "sonar.host.url", "https://sonarcloud.io" + } +} + +apply plugin: 'org.jlleitschuh.gradle.ktlint' +apply plugin: 'kotlin-android' +apply plugin: 'com.mparticle.kit' + +android { + namespace 'com.mparticle.kits.apptimize' + buildFeatures { + buildConfig = true + } + defaultConfig { + minSdkVersion 17 + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = '17' + } + testOptions { + unitTests.all { + jvmArgs += ['--add-opens', 'java.base/java.lang=ALL-UNNAMED'] + } + } +} + +repositories { + maven { url 'https://maven.apptimize.com/artifactory/repo' } + mavenCentral() +} + +dependencies { + api 'com.apptimize:apptimize-android:[3.10.18,4.0.0)@aar' +} diff --git a/kits/apptimize/apptimize-3/consumer-proguard.pro b/kits/apptimize/apptimize-3/consumer-proguard.pro new file mode 100644 index 000000000..0c29d3aa5 --- /dev/null +++ b/kits/apptimize/apptimize-3/consumer-proguard.pro @@ -0,0 +1,18 @@ +-keep class com.apptimize.** { *; } +-keepclassmembers class * extends com.apptimize.ApptimizeTest { + ; +} + +-keep class android.support.v4.view.ViewPager +-keepclassmembers class android.support.v4.view.ViewPager$LayoutParams { *; } +-keep class android.support.v4.app.Fragment { *; } + +-keep class com.mixpanel.android.mpmetrics.MixpanelAPI { *; } +-keep class com.google.android.gms.analytics.Tracker { *; } +-keep class com.google.analytics.tracking.android.Tracker { *; } +-keep class com.flurry.android.FlurryAgent { *; } +-keep class com.omniture.AppMeasurementBase { *; } +-keep class com.adobe.adms.measurement.ADMS_Measurement { *; } +-keep class com.adobe.mobile.Analytics { *; } +-keep class com.adobe.mobile.Config { *; } +-keep class com.localytics.android.Localytics { *; } \ No newline at end of file diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/build.gradle b/kits/apptimize/apptimize-3/example/example-kotlin/build.gradle new file mode 100644 index 000000000..166e95b33 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/build.gradle @@ -0,0 +1,46 @@ +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + namespace 'com.mparticle.kits.apptimize.example.kotlin' + compileSdk 35 + + defaultConfig { + applicationId 'com.mparticle.kits.apptimize.example.kotlin' + minSdk 17 + targetSdk 35 + versionCode 1 + versionName '1.0' + multiDexEnabled true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +repositories { + maven { url 'https://maven.apptimize.com/artifactory/repo' } + google() + mavenCentral() +} + +dependencies { + implementation project(':kits:apptimize:apptimize-3') + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.11.0' + implementation 'androidx.multidex:multidex:2.0.1' +} diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/AndroidManifest.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/AndroidManifest.xml new file mode 100644 index 000000000..84b1637c1 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/ExampleApplication.kt b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/ExampleApplication.kt new file mode 100644 index 000000000..7863f40a4 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/ExampleApplication.kt @@ -0,0 +1,24 @@ +package com.mparticle.kits.apptimize.example.kotlin + +import androidx.multidex.MultiDexApplication +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticleOptions + +class ExampleApplication : MultiDexApplication() { + override fun onCreate() { + super.onCreate() + val options = + MParticleOptions + .builder(this) + .credentials( + "REPLACE WITH YOUR MPARTICLE API KEY", + "REPLACE WITH YOUR MPARTICLE API SECRET", + ).logLevel(MParticle.LogLevel.VERBOSE) + .build() + MParticle.start(options) + MParticle.getInstance()?.logEvent( + MPEvent.Builder("foo", MParticle.EventType.Other).build(), + ) + } +} diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/MainActivity.kt b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/MainActivity.kt new file mode 100644 index 000000000..55c12ffb8 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/kotlin/com/mparticle/kits/apptimize/example/kotlin/MainActivity.kt @@ -0,0 +1,11 @@ +package com.mparticle.kits.apptimize.example.kotlin + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + } +} diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 000000000..046177833 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,14 @@ + + + + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/layout/activity_main.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/layout/activity_main.xml new file mode 100644 index 000000000..e2897e3ff --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/layout/activity_main.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 000000000..a8a8fa551 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/colors.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/colors.xml new file mode 100644 index 000000000..f42ada656 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/strings.xml b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/strings.xml new file mode 100644 index 000000000..f62870c13 --- /dev/null +++ b/kits/apptimize/apptimize-3/example/example-kotlin/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + Apptimize Kit Kotlin Example + diff --git a/kits/apptimize/apptimize-3/gradle.properties b/kits/apptimize/apptimize-3/gradle.properties new file mode 100644 index 000000000..edb1202c3 --- /dev/null +++ b/kits/apptimize/apptimize-3/gradle.properties @@ -0,0 +1,4 @@ +android.enableJetifier=true +android.useAndroidX=true +org.gradle.daemon=true +org.gradle.jvmargs=-Xmx2560m \ No newline at end of file diff --git a/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.jar b/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..41d9927a4 Binary files /dev/null and b/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.jar differ diff --git a/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.properties b/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..e1bef7e87 --- /dev/null +++ b/kits/apptimize/apptimize-3/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/kits/apptimize/apptimize-3/gradlew b/kits/apptimize/apptimize-3/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/kits/apptimize/apptimize-3/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/kits/apptimize/apptimize-3/gradlew.bat b/kits/apptimize/apptimize-3/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/kits/apptimize/apptimize-3/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/kits/apptimize/apptimize-3/settings.gradle.kts b/kits/apptimize/apptimize-3/settings.gradle.kts new file mode 100644 index 000000000..8d6361219 --- /dev/null +++ b/kits/apptimize/apptimize-3/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "android-apptimize-kit" +include(":") diff --git a/kits/apptimize/apptimize-3/src/main/AndroidManifest.xml b/kits/apptimize/apptimize-3/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c4e6c98d7 --- /dev/null +++ b/kits/apptimize/apptimize-3/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/kits/apptimize/apptimize-3/src/main/kotlin/com/mparticle/kits/ApptimizeKit.kt b/kits/apptimize/apptimize-3/src/main/kotlin/com/mparticle/kits/ApptimizeKit.kt new file mode 100644 index 000000000..12e6f75e8 --- /dev/null +++ b/kits/apptimize/apptimize-3/src/main/kotlin/com/mparticle/kits/ApptimizeKit.kt @@ -0,0 +1,347 @@ +package com.mparticle.kits + +import android.content.Context +import android.text.TextUtils +import com.apptimize.Apptimize +import com.apptimize.Apptimize.IsFirstTestRun +import com.apptimize.Apptimize.OnTestRunListener +import com.apptimize.ApptimizeOptions +import com.apptimize.ApptimizeTestInfo +import com.mparticle.MPEvent +import com.mparticle.MParticle +import com.mparticle.MParticle.IdentityType +import com.mparticle.commerce.CommerceEvent +import com.mparticle.consent.ConsentState +import com.mparticle.kits.KitIntegration.ModifyIdentityListener +import com.mparticle.kits.KitIntegration.UserAttributeListener +import com.mparticle.kits.KitIntegration.CommerceListener +import com.mparticle.kits.KitIntegration.LogoutListener +import java.math.BigDecimal + +class ApptimizeKit : + KitIntegration(), + UserAttributeListener, + ModifyIdentityListener, + LogoutListener, + KitIntegration.EventListener, + CommerceListener, + OnTestRunListener { + private fun toMessageList(message: ReportingMessage): List = listOf(message) + + private fun createReportingMessage(messageType: String): ReportingMessage = + ReportingMessage( + this, + messageType, + System.currentTimeMillis(), + null, + ) + + override fun onKitCreate( + settings: Map, + context: Context, + ): List { + val appKey = getSettings()[APP_MP_KEY] + require(!TextUtils.isEmpty(appKey)) { APP_MP_KEY } + val options = buildApptimizeOptions(settings) + Apptimize.setup(context, appKey, options) + if (java.lang.Boolean.parseBoolean(settings[TRACK_EXPERIMENTS])) { + Apptimize.setOnTestRunListener(this) + } + return emptyList() + } + + private fun buildApptimizeOptions(settings: Map): ApptimizeOptions { + val o = ApptimizeOptions() + o.isThirdPartyEventImportingEnabled = false + o.isThirdPartyEventExportingEnabled = false + configureApptimizeUpdateMetaDataTimeout(o, settings) + configureApptimizeDeviceName(o, settings) + configureApptimizeDeveloperModeDisabled(o, settings) + configureApptimizeExplicitEnablingRequired(o, settings) + configureApptimizeMultiprocessModeEnabled(o, settings) + configureApptimizeLogLevel(o, settings) + return o + } + + private fun configureApptimizeUpdateMetaDataTimeout( + o: ApptimizeOptions, + settings: Map, + ) { + try { + settings[UPDATE_METDATA_TIMEOUT_MP_KEY]?.let { + val l = it.toLong() + o.setUpdateMetadataTimeout(l) + } + } catch (nfe: NumberFormatException) { + } + } + + private fun configureApptimizeDeviceName( + o: ApptimizeOptions, + settings: Map, + ) { + val v = settings[DEVICE_NAME_MP_KEY] + o.deviceName = v + } + + private fun configureApptimizeDeveloperModeDisabled( + o: ApptimizeOptions, + settings: Map, + ) { + val b = settings[DEVELOPER_MODE_DISABLED_MP_KEY] + o.isDeveloperModeDisabled = b.toBoolean() + } + + private fun configureApptimizeExplicitEnablingRequired( + o: ApptimizeOptions, + settings: Map, + ) { + val b = settings[EXPLICIT_ENABLING_REQUIRED_MP_KEY] + o.isExplicitEnablingRequired = b.toBoolean() + } + + private fun configureApptimizeMultiprocessModeEnabled( + o: ApptimizeOptions, + settings: Map, + ) { + val b = settings[MULTIPROCESS_MODE_ENABLED_MP_KEY] + o.setMultiprocessMode(b.toBoolean()) + } + + private fun configureApptimizeLogLevel( + o: ApptimizeOptions, + settings: Map, + ) { + try { + val l = + settings[LOG_LEVEL_MP_KEY] + ?.let { ApptimizeOptions.LogLevel.valueOf(it) } + ?.let { o.logLevel = it } + } catch (iae: IllegalArgumentException) { + } catch (npe: NullPointerException) { + } + } + + override fun getName(): String = KIT_NAME + + override fun onSetUserAttributeList( + attributeKey: String?, + attributeValueList: List?, + ) { + // not supported by the Apptimize kit + } + + override fun supportsAttributeLists(): Boolean = false + + /** + * [userAttributeLists] is ignored by the Apptimize kit. + */ + override fun onSetAllUserAttributes( + userAttributes: Map, + userAttributeLists: Map>, + ) { + for ((key, value) in userAttributes) { + Apptimize.setUserAttribute(key, value) + } + } + + override fun onRemoveUserAttribute( + key: String, + ) { + Apptimize.clearUserAttribute(key) + } + + override fun onSetUserAttribute( + key: String?, + value: Any?, + ) { + if (key == null || value == null || value !is String) { + return + } + Apptimize.setUserAttribute(key, value) + } + + override fun onIncrementUserAttribute( + key: String?, + incrementedBy: Number?, + value: String?, + ) { + } + + override fun onSetUserTag( + key: String?, + ) { + } + + override fun onConsentStateUpdated( + oldState: ConsentState?, + newState: ConsentState?, + ) { + } + + /** + * @param identityType only Alias and CustomerId are suppoted by the Apptimize kit. + */ + override fun setUserIdentity( + identityType: IdentityType, + id: String?, + ) { + when (identityType) { + IdentityType.Alias, IdentityType.CustomerId -> { + Apptimize.setPilotTargetingId(id) + } + else -> {} + } + } + + override fun removeUserIdentity(identityType: IdentityType) { + setUserIdentity(identityType, null) + } + + override fun logout(): List { + Apptimize.track(LOGOUT_TAG) + return toMessageList(ReportingMessage.logoutMessage(this)) + } + + /** + * Not supported by the Apptimize kit. + */ + override fun leaveBreadcrumb(s: String): List = emptyList() + + /** + * Not supported by the Apptimize kit. + */ + override fun logError( + s: String, + map: Map, + ): List = emptyList() + + /** + * Not supported by the Apptimize kit. + */ + override fun logException( + e: Exception, + map: Map, + s: String, + ): List = emptyList() + + override fun logEvent(mpEvent: MPEvent): List { + Apptimize.track(mpEvent.eventName) + return toMessageList(ReportingMessage.fromEvent(this, mpEvent)) + } + + /** + * @param eventAttributes is ignored by the Apptimize kit. + */ + override fun logScreen( + screenName: String, + eventAttributes: Map, + ): List { + val event = String.format(VIEWED_EVENT_FORMAT, screenName) + Apptimize.track(event) + return toMessageList( + createReportingMessage(ReportingMessage.MessageType.SCREEN_VIEW).setScreenName( + screenName, + ), + ) + } + + /** + * @param valueTotal is ignored by the Apptimize kit. + * @param contextInfo is ignored by the Apptimize kit. + */ + override fun logLtvIncrease( + valueIncreased: BigDecimal, + valueTotal: BigDecimal, + eventName: String, + contextInfo: Map, + ): List { + // match the iOS style, where only the delta is sent rather than an absolute final value. + Apptimize.track(LTV_TAG, valueIncreased.toDouble()) + return toMessageList(createReportingMessage(ReportingMessage.MessageType.COMMERCE_EVENT)) + } + + override fun logEvent(commerceEvent: CommerceEvent): List? { + val customEvents = CommerceEventUtils.expand(commerceEvent) + if (customEvents.size == 0) { + return null + } + for (event in customEvents) { + Apptimize.track(event.eventName) + } + return toMessageList(ReportingMessage.fromEvent(this, commerceEvent)) + } + + /** + * After opting out, it is not possible to opt back in via the Apptimize kit. + * @param optedOut only a value of 'true' supported by the Apptimize kit. + */ + override fun setOptOut(optedOut: Boolean): List? { + var ret: List? = null + if (optedOut) { + Apptimize.disable() + ret = + toMessageList( + createReportingMessage(ReportingMessage.MessageType.OPT_OUT).setOptOut(optedOut), + ) + } + return ret + } + + override fun onTestRun( + apptimizeTestInfo: ApptimizeTestInfo, + isFirstTestRun: IsFirstTestRun, + ) { + if (isFirstTestRun != IsFirstTestRun.YES) { + return + } + val testInfoMap = Apptimize.getTestInfo() + val participatedExperiments: MutableList = ArrayList() + if (testInfoMap == null) { + return + } + for (key in testInfoMap.keys) { + val testInfo = testInfoMap[key] + if (testInfo != null) { + if (testInfo.userHasParticipated()) { + val nameAndVariation = testInfo.testName + "-" + testInfo.enrolledVariantName + participatedExperiments.add(nameAndVariation) + } + } + } + val user = MParticle.getInstance()!!.Identity().currentUser + user?.setUserAttributeList("Apptimize experiment", participatedExperiments) + val eventInfo = HashMap(5) + eventInfo["VariationID"] = apptimizeTestInfo.enrolledVariantId.toString() + eventInfo["ID"] = apptimizeTestInfo.testId.toString() + eventInfo["Name"] = apptimizeTestInfo.testName + eventInfo["Variation"] = apptimizeTestInfo.enrolledVariantName + eventInfo["Name and Variation"] = + apptimizeTestInfo.testName + "-" + + apptimizeTestInfo.enrolledVariantName + val event = + MPEvent + .Builder( + "Apptimize experiment", + MParticle.EventType + .Other, + ).customAttributes(eventInfo) + .build() + MParticle.getInstance()?.logEvent(event) + } + + companion object { + private const val APP_MP_KEY = "appKey" + private const val UPDATE_METDATA_TIMEOUT_MP_KEY = "metadataTimeout" + private const val DEVICE_NAME_MP_KEY = "deviceName" + private const val DEVELOPER_MODE_DISABLED_MP_KEY = "developerModeDisabled" + private const val EXPLICIT_ENABLING_REQUIRED_MP_KEY = "explicitEnablingRequired" + private const val MULTIPROCESS_MODE_ENABLED_MP_KEY = "multiprocessModeEnabled" + private const val LOG_LEVEL_MP_KEY = "logLevel" + private const val LOGOUT_TAG = "logout" + private const val LTV_TAG = "ltv" + private const val VIEWED_EVENT_FORMAT = "screenView %s" + private const val TRACK_EXPERIMENTS = "trackExperiments" + private const val KIT_NAME = "Apptimize" + } +} diff --git a/kits/apptimize/apptimize-3/src/test/kotlin/com/mparticle/kits/ApptimizeKitTests.kt b/kits/apptimize/apptimize-3/src/test/kotlin/com/mparticle/kits/ApptimizeKitTests.kt new file mode 100644 index 000000000..e79045702 --- /dev/null +++ b/kits/apptimize/apptimize-3/src/test/kotlin/com/mparticle/kits/ApptimizeKitTests.kt @@ -0,0 +1,53 @@ +package com.mparticle.kits + +import android.content.Context +import com.mparticle.MParticleOptions +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito + +class ApptimizeKitTests { + private val kit: KitIntegration + get() = ApptimizeKit() + + @Test + @Throws(Exception::class) + fun testGetName() { + val name = kit.name + Assert.assertTrue(!name.isNullOrEmpty()) + } + + /** + * Kit *should* throw an exception when they're initialized with the wrong settings. + * + */ + @Test + @Throws(Exception::class) + fun testOnKitCreate() { + var e: Exception? = null + try { + val kit = kit + val settings = HashMap() + settings["fake setting"] = "fake" + kit.onKitCreate(settings, Mockito.mock(Context::class.java)) + } catch (ex: Exception) { + e = ex + } + Assert.assertNotNull(e) + } + + @Test + @Throws(Exception::class) + fun testClassName() { + val options = Mockito.mock(MParticleOptions::class.java) + val factory = KitIntegrationFactory(options) + val integrations = factory.supportedKits.values + val className = kit.javaClass.name + for (integration in integrations) { + if (integration.name == className) { + return + } + } + Assert.fail("$className not found as a known integration.") + } +} diff --git a/kits/apteligent-kit b/kits/apteligent-kit deleted file mode 160000 index 3a672a770..000000000 --- a/kits/apteligent-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3a672a770668a38b6e641f0b217d1bb68aecd912 diff --git a/kits/branch-kit b/kits/branch-kit deleted file mode 160000 index 3511c3183..000000000 --- a/kits/branch-kit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3511c318337952bd80c880a02ad6fd6ed84441fb diff --git a/kits/branch/branch-5/README.md b/kits/branch/branch-5/README.md new file mode 100644 index 000000000..1f6368641 --- /dev/null +++ b/kits/branch/branch-5/README.md @@ -0,0 +1,24 @@ +# Branch Kit Integration + +This repository contains the [Branch](https://www.branch.io/) integration for the [mParticle Android SDK](https://github.com/mParticle/mparticle-android-sdk). + +## Adding the integration + +1. Add the kit dependency to your app's build.gradle: + + ```groovy + dependencies { + implementation 'com.mparticle:android-branch-kit:5+' + } + ``` + +2. Follow the mParticle Android SDK [quick-start](https://github.com/mParticle/mparticle-android-sdk), then rebuild and launch your app, and verify that you see `"Branch Metrics detected"` in the output of `adb logcat`. +3. Reference mParticle's integration docs below to enable the integration. + +## Documentation + +[Branch integration](https://docs.mparticle.com/integrations/branch-metrics/event/) + +## License + +[Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/kits/branch/branch-5/SampleApplication/.gitignore b/kits/branch/branch-5/SampleApplication/.gitignore new file mode 100644 index 000000000..39fb081a4 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/kits/branch/branch-5/SampleApplication/README.md b/kits/branch/branch-5/SampleApplication/README.md new file mode 100644 index 000000000..ab7025030 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/README.md @@ -0,0 +1,13 @@ +# mParticle-Branch Example + +## Overview + +This sample app demonstrates: + +• How to use the mParticle and Branch in a simple Android application. + +• How to create and share Branch links in your app. + +• How to Branch deep links are handled in an app when using branch with mParticle. + +• How event tracking is handled in mParticle and Branch. diff --git a/kits/branch/branch-5/SampleApplication/app/.gitignore b/kits/branch/branch-5/SampleApplication/app/.gitignore new file mode 100644 index 000000000..796b96d1c --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kits/branch/branch-5/SampleApplication/app/build.gradle b/kits/branch/branch-5/SampleApplication/app/build.gradle new file mode 100644 index 000000000..4f749ed58 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/build.gradle @@ -0,0 +1,43 @@ +apply plugin: 'com.android.application' + +android { + compileSdk = 34 + buildToolsVersion "27.0.3" + defaultConfig { + applicationId "com.mparticle.branchsample" + minSdk = 25 + targetSdk = 33 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + packagingOptions { + exclude 'META-INF/proguard/androidx-annotations.pro' + exclude 'META-INF/versions/9/module-info.class' + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.8.0' + implementation 'com.android.installreferrer:installreferrer:2.2' + + // Local project dependency + api project(':local-mParticle-Branch-kit') + + // Uncomment below to use published version instead + // implementation 'com.mparticle:android-branch-kit:5+' + + testImplementation 'junit:junit:4.13.2' + + androidTestImplementation('androidx.test.espresso:espresso-core:3.5.1', { + exclude group: 'androidx.test', module: 'runner' + }) +} diff --git a/kits/branch/branch-5/SampleApplication/app/proguard-rules.pro b/kits/branch/branch-5/SampleApplication/app/proguard-rules.pro new file mode 100644 index 000000000..2d3430033 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/proguard-rules.pro @@ -0,0 +1,16 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# 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: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/kits/branch/branch-5/SampleApplication/app/src/androidTest/java/com/mparticle/branchsample/ExampleInstrumentedTest.java b/kits/branch/branch-5/SampleApplication/app/src/androidTest/java/com/mparticle/branchsample/ExampleInstrumentedTest.java new file mode 100644 index 000000000..6cb710a35 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/androidTest/java/com/mparticle/branchsample/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.mparticle.branchsample; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumentation test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getTargetContext(); + + assertEquals("com.mparticle.branchsample", appContext.getPackageName()); + } +} diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/AndroidManifest.xml b/kits/branch/branch-5/SampleApplication/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a81275e7e --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/SampleApplication.java b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/SampleApplication.java new file mode 100644 index 000000000..65b473293 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/SampleApplication.java @@ -0,0 +1,101 @@ +package com.mparticle.branchsample; + +import android.app.Application; + +import com.mparticle.AttributionError; +import com.mparticle.AttributionListener; +import com.mparticle.AttributionResult; +import com.mparticle.MParticle; +import com.mparticle.MParticleOptions; + +import org.json.JSONObject; + +import io.branch.referral.Branch; + +public class SampleApplication extends Application implements AttributionListener { + private IBranchEvents branchEventCallback; + private static SampleApplication instance; + + @Override + public void onCreate() { + super.onCreate(); + instance = this; + MParticleOptions options = MParticleOptions.builder(this) + .credentials("REPLACE_ME_WITH_API_KEY", "REPLACE_ME_WITH_API_SECRET") + .attributionListener(this) + //Set an instance of AttributionListener + //this instance will continually be called whenever + //there is new attribution/deep linking information. + //It is essentially equivalent to the Branch SDK's BranchReferralInitListener class + //that is passed to the Branch SDK's initSession() API. + .logLevel(MParticle.LogLevel.VERBOSE) + .build(); + + MParticle.start(options); + } + + public static void setBranchEventCallback(IBranchEvents branchEventCallback) { + instance.branchEventCallback = branchEventCallback; + } + + + /** + * A few typical scenarios where this callback would be invoked: + *

+ * (1) Base case: + * - User does not tap on a link, and then opens the app (either after a fresh install or not) + * - This block will be invoked with Branch Metrics' response indicating that this user did not tap on a link. + *

+ * (2) Deferred deep link: + * - User without the app installed taps on a link + * - User is redirected from Branch Metrics to the App Store and installs the app + * - User opens the app + * - This block will be invoked with Branch Metrics' response containing the details of the link + *

+ * (3) Deep link with app installed: + * - User with the app already installed taps on a link + * - Application opens directly to an Activity via a link click, mParticle forwards the launch URI etc to Branch + * - This callback will be invoked with Branch Metrics' response containing the details of the link + *

+ * If the user navigates away from the app without killing it, this callback could be invoked several times: + * once for the initial launch, and then again each time the user taps on a link to re-open the app. + * + *

+ * This method is equivalent to {@link io.branch.referral.Branch#initSession(Branch.BranchReferralInitListener)}. + * This will return deep links params if the app is opened by a link click otherwise a JSONObject with ""+clicked_branch_link = false". + * The below example shows how you can message this to your Activities listening for Branch deep link params. + *

+ * + * @param attributionResult + **/ + @Override + public void onResult(AttributionResult attributionResult) { + //this will be invoked for + if (attributionResult.getServiceProviderId() == MParticle.ServiceProviders.BRANCH_METRICS) { + JSONObject params = attributionResult.getParameters(); + // The Branch SDK will return a response for all new session/app opens, even + // if the user did not click a branch link. + // The parameters supported by the Branch SDK are documented here: + // https://github.com/BranchMetrics/android-branch-deep-linking#branch-provided-data-parameters-in-initsession-callback + // For eg: +// if (params.optBoolean("+clicked_branch_link", false)) { +// //handle the Branch link click +// } + if (branchEventCallback != null) { + branchEventCallback.onBranchInitialised(params); + } + } + } + + @Override + public void onError(AttributionError attributionError) { + //if the Branch SDK returns an error, it will be surfaced here. + if (branchEventCallback != null) { + branchEventCallback.onBranchInitialised(new JSONObject()); + } + } + + public interface IBranchEvents { + void onBranchInitialised(JSONObject params); + } +} diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/HomeActivity.java b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/HomeActivity.java new file mode 100644 index 000000000..dd8a9ac04 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/HomeActivity.java @@ -0,0 +1,272 @@ +package com.mparticle.branchsample.activities; + +import android.content.Intent; +import android.os.Bundle; +import androidx.appcompat.app.AppCompatActivity; +import android.text.TextUtils; +import android.view.View; +import android.widget.CompoundButton; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.ToggleButton; + +import com.mparticle.MPEvent; +import com.mparticle.MParticle; +import com.mparticle.branchsample.R; +import com.mparticle.commerce.CommerceEvent; +import com.mparticle.commerce.Impression; +import com.mparticle.commerce.Product; +import com.mparticle.commerce.TransactionAttributes; +import com.mparticle.identity.IdentityApiRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import io.branch.indexing.BranchUniversalObject; +import io.branch.referral.SharingHelper; +import io.branch.referral.util.BranchContentSchema; +import io.branch.referral.util.ContentMetadata; +import io.branch.referral.util.CurrencyType; +import io.branch.referral.util.LinkProperties; +import io.branch.referral.util.ProductCategory; +import io.branch.referral.util.ShareSheetStyle; + +public class HomeActivity extends AppCompatActivity { + public static final String BRANCH_PARAMS = "branch_params"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.home_activity); + ((ToggleButton) findViewById(R.id.tracking_cntrl_btn)).setChecked(MParticle.getInstance().getOptOut()); + + findViewById(R.id.cmdSetIdentity).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MParticle.getInstance().Identity().login(IdentityApiRequest.withEmptyUser() + .email("foo@example.com") + .customerId("12332424555") + .build()); + } + }); + + findViewById(R.id.cmdLogout).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + MParticle.getInstance().Identity().logout(); + } + }); + + findViewById(R.id.cmdTrackView).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + logScreen(); + } + }); + + findViewById(R.id.cmdLogSimpleEvent).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + logSimpleEvent(); + } + }); + + findViewById(R.id.cmdShareBtn).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + shareBranchLink(); + } + }); + + findViewById(R.id.cmdTrackEvent).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + logCommerceEvent((String) ((Spinner) findViewById(R.id.event_name_spinner)).getSelectedItem()); + } + }); + + ((ToggleButton) findViewById(R.id.tracking_cntrl_btn)).setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { + MParticle.getInstance().setOptOut(isChecked); + } + }); + + } + + @Override + protected void onStart() { + super.onStart(); + ((TextView) findViewById(R.id.deep_link_params_txt)).setText(""); + } + + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + if (!TextUtils.isEmpty(intent.getStringExtra(BRANCH_PARAMS))) { + String deepLinkParams = getIntent().getStringExtra(BRANCH_PARAMS); + try { + String deepLikMsg = "Branch Deep Link Params \n\n" + new JSONObject(deepLinkParams).toString(4); + ((TextView) findViewById(R.id.deep_link_params_txt)).setText(deepLikMsg); + } catch (JSONException e) { + e.printStackTrace(); + } + intent.removeExtra(BRANCH_PARAMS); + setIntent(intent); + } else { + ((TextView) findViewById(R.id.deep_link_params_txt)).setText(""); + } + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + } + + private void logScreen() { + Map eventInfo = new HashMap<>(2); + eventInfo.put("screen_attr_key1", "screen_attr_val1"); + eventInfo.put("screen_attr_key2", "screen_attr_val2"); + MParticle.getInstance().logScreen("SecondActivity", eventInfo); + } + + private void logSimpleEvent() { + Map eventInfo = new HashMap<>(2); + eventInfo.put("custom_attr_key1", "custom_attr_val1"); + eventInfo.put("custom_attr_key2", "custom_attr_val2"); + + MPEvent event = new MPEvent.Builder("Simple Event", MParticle.EventType.Transaction) + .duration(100) + .customAttributes(eventInfo) + .category("Food and Beverages") + .build(); + MParticle.getInstance().logEvent(event); + } + + private void logCommerceEvent(String eventName) { + + Map customAttr = new HashMap<>(2); + customAttr.put("custom_attr_key1", "custom_attr_val1"); + customAttr.put("custom_attr_key2", "custom_attr_val2"); + + Product product1 = new Product.Builder("Prod1", "my_sku", 100.00) + .brand("my_prod_brand") + .category("my_prod_category") + .couponCode("my_coupon_code") + .customAttributes(customAttr) + .name("my_prod_name") + .position(1) + .quantity(2.5) + .sku("my_sku") + .unitPrice(12.5) + .variant("my_variant") + .quantity(4) + .build(); + + Product product2 = new Product.Builder("Impression_prod", "my_sku", 100.00) + .brand("my_prod_brand") + .category("my_prod_category") + .couponCode("my_coupon_code") + .customAttributes(customAttr) + .name("my_prod_name") + .position(1) + .quantity(2.5) + .sku("my_sku") + .unitPrice(12.5) + .variant("my_variant") + .quantity(4) + .build(); + + Product product3 = new Product.Builder("prod3", "my_sku", 100.00) + .brand("my_prod_brand") + .category("my_prod_category") + .couponCode("my_coupon_code") + .customAttributes(customAttr) + .name("my_prod_name") + .position(1) + .quantity(2.5) + .sku("my_sku") + .unitPrice(12.5) + .variant("my_variant") + .quantity(4) + .build(); + + TransactionAttributes attributes = new TransactionAttributes("foo-transaction-id") + .setCouponCode("transaction_coupon_code") + .setAffiliation("transaction_affiliation") + .setId("transaction_id") + .setRevenue(13.5) + .setShipping(3.5) + .setTax(4.5); + + Impression impression = new Impression("Impression", product2); + + CommerceEvent commerceEvent = new CommerceEvent.Builder(eventName, product1) + .currency("USD") + .customAttributes(customAttr) + .transactionAttributes(attributes) + .addImpression(impression) + .productListName("my_commerce_event_prod_list") + .addProduct(product3) + .build(); + MParticle.getInstance().logEvent(commerceEvent); + } + + private void shareBranchLink() { + BranchUniversalObject buo = new BranchUniversalObject() + .setCanonicalIdentifier("item/12345") + .setCanonicalUrl("https://branch.io/deepviews") + .setContentIndexingMode(BranchUniversalObject.CONTENT_INDEX_MODE.PRIVATE) + .setLocalIndexMode(BranchUniversalObject.CONTENT_INDEX_MODE.PUBLIC) + .setTitle("My Content Title") + .setContentDescription("my_product_description1") + .setContentImageUrl("https://example.com/mycontent-12345.png") + .setContentExpiration(new Date(1573415635000L)) + .setContentImageUrl("https://test_img_url") + .addKeyWord("My_Keyword1") + .addKeyWord("My_Keyword2") + .setContentMetadata( + new ContentMetadata().setProductName("my_product_name1") + .setProductBrand("my_prod_Brand1") + .setProductVariant("3T") + .setProductCategory(ProductCategory.BABY_AND_TODDLER) + .setProductCondition(ContentMetadata.CONDITION.EXCELLENT) + .setAddress("Street_name1", "city1", "Region1", "Country1", "postal_code") + .setLocation(12.07, -97.5) + .setSku("1994320302") + .setRating(6.0, 5.0, 7.0, 5) + .addImageCaptions("my_img_caption1", "my_img_caption_2") + .setQuantity(2.0) + .setPrice(23.2, CurrencyType.USD) + .setContentSchema(BranchContentSchema.COMMERCE_PRODUCT) + .addCustomMetadata("Custom_Content_metadata_key1", "Custom_Content_metadata_val1") + ); + + + LinkProperties linkProperties = new LinkProperties() + .addTag("Tag1") + .setChannel("Sharing_Channel_name") + .setFeature("my_feature_name") + .addControlParameter("$android_deeplink_path", "custom/path/*") + .addControlParameter("$ios_url", "http://example.com/ios") + .setDuration(100); + + ShareSheetStyle shareSheetStyle = new ShareSheetStyle(this, "My Sharing Message Title", "My Sharing message body") + .setCopyUrlStyle(getResources().getDrawable(android.R.drawable.ic_menu_send), "Save this URl", "Link added to clipboard") + .setMoreOptionStyle(getResources().getDrawable(android.R.drawable.ic_menu_search), "Show more") + .addPreferredSharingOption(SharingHelper.SHARE_WITH.FACEBOOK) + .addPreferredSharingOption(SharingHelper.SHARE_WITH.EMAIL) + .addPreferredSharingOption(SharingHelper.SHARE_WITH.MESSAGE) + .addPreferredSharingOption(SharingHelper.SHARE_WITH.TWITTER) + .setAsFullWidthStyle(true) + .setSharingTitle("Share With"); + + buo.showShareSheet(this, linkProperties, shareSheetStyle, null); + } + +} diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/SplashActivity.java b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/SplashActivity.java new file mode 100644 index 000000000..ee688119b --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/main/java/com/mparticle/branchsample/activities/SplashActivity.java @@ -0,0 +1,81 @@ +package com.mparticle.branchsample.activities; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import com.mparticle.MPEvent; +import com.mparticle.MParticle; +import com.mparticle.branchsample.R; +import com.mparticle.branchsample.SampleApplication; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by sojanpr on 4/18/18. + *

+ * Splash Activity for MParticle-Branch Kit integration + *

+ */ + +public class SplashActivity extends AppCompatActivity implements SampleApplication.IBranchEvents { + + private static final int SPLASH_DELAY = 1500; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.splash_activity); + SampleApplication.setBranchEventCallback(this); + } + + @Override + protected void onStart() { + super.onStart(); + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + + + } + }, SPLASH_DELAY); + } + + /** + * Note : Branch needs the new intents to be set to the activity if the launch mode is `SingleTask` + **/ + @Override + protected void onNewIntent(Intent intent) { + this.setIntent(intent); + } + + @Override + public void onBranchInitialised(JSONObject params) { + final Intent intent = new Intent(SplashActivity.this, HomeActivity.class); + if (params.optBoolean("+clicked_branch_link")) { + Map infoMap = new HashMap<>(); + infoMap.put("Referred Link", params.optString("~referring_link")); + + MPEvent event = new MPEvent.Builder("Referred Session", MParticle.EventType.UserContent) + .duration(300) + .customAttributes(infoMap) + .category("Session").build(); + MParticle.getInstance().logEvent(event); + intent.putExtra(HomeActivity.BRANCH_PARAMS, params.toString()); + } + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + SplashActivity.this.startActivity(intent); + SplashActivity.this.finish(); + } + }, 1500); + } + +} diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/res/drawable/app_icon.png b/kits/branch/branch-5/SampleApplication/app/src/main/res/drawable/app_icon.png new file mode 100644 index 000000000..e1148ad54 Binary files /dev/null and b/kits/branch/branch-5/SampleApplication/app/src/main/res/drawable/app_icon.png differ diff --git a/kits/branch/branch-5/SampleApplication/app/src/main/res/layout/home_activity.xml b/kits/branch/branch-5/SampleApplication/app/src/main/res/layout/home_activity.xml new file mode 100644 index 000000000..8bce436d0 --- /dev/null +++ b/kits/branch/branch-5/SampleApplication/app/src/main/res/layout/home_activity.xml @@ -0,0 +1,128 @@ + + + + + + + +