diff --git a/.github/ISSUE_TEMPLATE/BUG-REPORT.yml b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml new file mode 100644 index 00000000..2334b6c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG-REPORT.yml @@ -0,0 +1,82 @@ +name: 🐞 Bug +description: File a bug/issue +title: "[BUG] " +labels: ["bug", "needs-triage"] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue already exists for the bug you encountered. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SDK Version + description: Version of the SDK in use? + validations: + required: true +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what you're experiencing. + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. In this environment... + 2. With this config... + 3. Run '...' + 4. See error... + validations: + required: true +- type: textarea + attributes: + label: Link + description: Link to code demonstrating the problem. + validations: + required: false +- type: textarea + attributes: + label: Logs + description: Logs/stack traces related to the problem (⚠️do not include sensitive information). + validations: + required: false +- type: dropdown + attributes: + label: Severity + description: What is the severity of the problem? + multiple: true + options: + - Blocking development + - Affecting users + - Minor issue + validations: + required: false +- type: textarea + attributes: + label: Workaround/Solution + description: Do you have any workaround or solution in mind for the problem? + validations: + required: false +- type: textarea + attributes: + label: "Recent Change" + description: Has this issue started happening after an update or experiment change? + validations: + required: false +- type: textarea + attributes: + label: Conflicts + description: Are there other libraries/dependencies potentially in conflict? + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml new file mode 100644 index 00000000..79c53247 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -0,0 +1,45 @@ +name: ✨Enhancement +description: Create a new ticket for a Enhancement/Tech-initiative for the benefit of the SDK which would be considered for a minor version update. +title: "[ENHANCEMENT] <title>" +labels: ["enhancement"] +body: + - type: textarea + id: description + attributes: + label: "Description" + description: Briefly describe the enhancement in a few sentences. + placeholder: Short description... + validations: + required: true + - type: textarea + id: benefits + attributes: + label: "Benefits" + description: How would the enhancement benefit to your product or usage? + placeholder: Benefits... + validations: + required: true + - type: textarea + id: detail + attributes: + label: "Detail" + description: How would you like the enhancement to work? Please provide as much detail as possible + placeholder: Detailed description... + validations: + required: false + - type: textarea + id: examples + attributes: + label: "Examples" + description: Are there any examples of this enhancement in other products/services? If so, please provide links or references. + placeholder: Links/References... + validations: + required: false + - type: textarea + id: risks + attributes: + label: "Risks/Downsides" + description: Do you think this enhancement could have any potential downsides or risks? + placeholder: Risks/Downsides... + validations: + required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md new file mode 100644 index 00000000..a061f335 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md @@ -0,0 +1,4 @@ +<!-- + Thanks for filing in issue! Are you requesting a new feature? If so, please share your feedback with us on the following link. +--> +## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..d28ef3dd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: 💡Feature Requests + url: https://feedback.optimizely.com/ + about: Feedback requesting a new feature can be shared here. \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 5d0cc830..e1e3820a 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -16,7 +16,7 @@ jobs: lint_markdown_files: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Ruby uses: ruby/setup-ruby@v1 with: @@ -31,24 +31,26 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/') != true && github.event.inputs.SNAPSHOT != 'true' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: # You should create a personal access token and store it in your repository token: ${{ secrets.CI_USER_TOKEN }} - repository: 'optimizely/travisci-tools' - path: 'home/runner/travisci-tools' + repository: 'optimizely/ci-helper-tools' + path: 'home/runner/ci-helper-tools' ref: 'master' - name: set SDK Branch if PR + env: + HEAD_REF: ${{ github.head_ref }} if: ${{ github.event_name == 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV - echo "TRAVIS_BRANCH=${{ github.head_ref }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$HEAD_REF" >> $GITHUB_ENV - name: set SDK Branch if not pull request + env: + REF_NAME: ${{github.ref_name}} if: ${{ github.event_name != 'pull_request' }} run: | - echo "SDK_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV - echo "TRAVIS_BRANCH=${{ github.ref_name }}" >> $GITHUB_ENV + echo "SDK_BRANCH=$REF_NAME" >> $GITHUB_ENV - name: Trigger build env: SDK: android @@ -64,41 +66,46 @@ jobs: PULL_REQUEST_SHA: ${{ github.event.pull_request.head.sha }} PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }} UPSTREAM_SHA: ${{ github.sha }} - TOKEN: ${{ secrets.TRAVIS_COM_TOKEN }} EVENT_MESSAGE: ${{ github.event.message }} HOME: 'home/runner' run: | echo "$GITHUB_CONTEXT" - home/runner/travisci-tools/trigger-script-with-status-update.sh + home/runner/ci-helper-tools/trigger-script-with-status-update.sh build: uses: optimizely/android-sdk/.github/workflows/build.yml@master with: action: build test: if: ${{ startsWith(github.ref, 'refs/tags/') != true && github.event.inputs.SNAPSHOT != 'true' }} - runs-on: macos-latest + runs-on: ubuntu-latest strategy: fail-fast: false matrix: api-level: [21, 25, 26, 29] steps: - name: checkout - uses: actions/checkout@v2 - - name: set up JDK 11 + uses: actions/checkout@v4 + - name: set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: '17' + - name: Enable KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Gradle cache - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}-${{ hashFiles('**/gradle/wrapper/gradle-wrapper.properties') }}-${{ hashFiles('**/buildSrc/**/*.kt') }} - name: AVD cache - uses: actions/cache@v2 + uses: actions/cache@v4 id: avd-cache with: path: | @@ -112,6 +119,7 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: ${{ matrix.api-level }} + # arch: arm64-v8a # Specify ARM architecture force-avd-creation: false emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none disable-animations: false @@ -124,13 +132,13 @@ jobs: 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: ./gradlew testAllModulesTravis + script: ./gradlew testAllModules publish: if: startsWith(github.ref, 'refs/tags/') uses: optimizely/android-sdk/.github/workflows/build.yml@master with: action: ship - travis_tag: ${GITHUB_REF#refs/*/} + github_tag: ${GITHUB_REF#refs/*/} secrets: MAVEN_SIGNING_KEY_BASE64: ${{ secrets.MAVEN_SIGNING_KEY_BASE64 }} MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }} @@ -142,7 +150,7 @@ jobs: uses: optimizely/android-sdk/.github/workflows/build.yml@master with: action: ship - travis_tag: BB-SNAPSHOT + github_tag: BB-SNAPSHOT secrets: MAVEN_SIGNING_KEY_BASE64: ${{ secrets.MAVEN_SIGNING_KEY_BASE64 }} MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index acbab681..659a2b5b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ on: action: required: true type: string - travis_tag: + github_tag: required: false type: string secrets: @@ -22,13 +22,16 @@ jobs: run_build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: set up JDK 11 - uses: actions/setup-java@v2 + - uses: actions/checkout@v4 + - name: set up JDK 17 + uses: actions/setup-java@v4 with: - java-version: '11' + java-version: '17' distribution: 'temurin' - cache: gradle + - name: Setup Gradle cache + uses: gradle/gradle-build-action@v2 + with: + gradle-version: wrapper - name: Grant execute permission for gradlew run: chmod +x gradlew - name: Clean all modules @@ -39,4 +42,4 @@ jobs: MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }} MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} - run: TRAVIS_TAG=${{ inputs.travis_tag }} ./gradlew ${{ inputs.action }} + run: GITHUB_TAG=${{ inputs.github_tag }} ./gradlew ${{ inputs.action }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0de104e5..bb6e2cc5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,156 @@ # Optimizely Android X SDK Changelog +## 5.0.1 +May 30th, 2025 + +### Functionality Enhancements +* Add experimentId and variationId to decision notification ([#509](https://github.com/optimizely/android-sdk/pull/509)). + +## 5.0.0 +November 25th, 2024 + +### Breaking Changes +* VUID configuration is now independent of ODP ([#497](https://github.com/optimizely/android-sdk/pull/497)) +* When VUID is disabled: + * `vuid` is not generated or saved. + * `client-initialized` event will not auto fired on SDK init. + * `vuid` is not included in the odp events as a default attribute. + * `createUserContext()` will be rejected if `userId` is not provided. + +## 4.1.0 +November 13th, 2024 + +### New Features +* Batch UPS lookup and save calls in decideAll and decideForKeys methods ([#498](https://github.com/optimizely/android-sdk/pull/498)). + +## 4.0.4 +September 10th, 2024 + +### Bug Fixes +* R8 configuration breaks Gson use at runtime ([#493](https://github.com/optimizely/android-sdk/pull/493)). + + +## 4.0.0 +January 17th, 2024 + +### New Features + +The 4.0.0 release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) +([#431](https://github.com/optimizely/android-sdk/pull/431), +[#440](https://github.com/optimizely/android-sdk/pull/440), +[#444](https://github.com/optimizely/android-sdk/pull/444), +[#445](https://github.com/optimizely/android-sdk/pull/445), +[#448](https://github.com/optimizely/android-sdk/pull/448), +[#470](https://github.com/optimizely/android-sdk/pull/470)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Customer Success Manager. + +This version includes the following changes: + +* New API added to `OptimizelyUserContext`: + - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays. + - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities. + +* New APIs added to `OptimizelyClient`: + - `sendODPEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + - `createUserContext()` with anonymous user IDs: user-contexts can be created without a userId. The SDK will create and use a persistent `VUID` specific to a device when userId is not provided. + +For details, refer to our documentation pages: + +* [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +* [Client SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-client-side-sdks) + +* [Initialize Android SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-android) + +* [OptimizelyUserContext Android SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-android) + +* [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-android) + +* [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-android) + +### Breaking Changes + +* `ODPManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `ODPManager` functions will be ignored. If needed, `ODPManager` can be disabled when `OptimizelyClient` is instantiated. +* minimum Android API level requirements upgraded to 21 or higher. + +### Bug Fixes +* support arbitrary client names to be included in logx and odp events. ([#459](https://github.com/optimizely/android-sdk/pull/459)). +* Added catch block to capture resource not found exception. ([#460](https://github.com/optimizely/android-sdk/pull/460)). +* Added a proguard rule to suppress warning for java.beans.Transient. Upgraded Java to 11. ([#471](https://github.com/optimizely/android-sdk/pull/471)). +* Added a proguard rule to keep ODPEvent and added sample codes for ODP. ([#456](https://github.com/optimizely/android-sdk/pull/456)). + +### Functionality Enhancements +- Update Github Issue Templates ([#461](https://github.com/optimizely/android-sdk/pull/461)) + + +## 4.0.0-beta3 +September 20th, 2023 + +### Bug Fixes +* support arbitrary client names to be included in logx and odp events. ([#459](https://github.com/optimizely/android-sdk/pull/459)). +* Added catch block to capture resource not found exception. ([#460](https://github.com/optimizely/android-sdk/pull/460)). + +### Functionality Enhancements +- Update Github Issue Templates ([#461](https://github.com/optimizely/android-sdk/pull/461)) + + +## 4.0.0-beta2 +May 8th, 2023 + +### Bug Fixes +* Added a proguard rule to keep ODPEvent and added sample codes for ODP. ([#456](https://github.com/optimizely/android-sdk/pull/456)). + + +## 4.0.0-beta +May 4th, 2023 + +### New Features + +The 4.0.0-beta release introduces a new primary feature, [Advanced Audience Targeting]( https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) enabled through integration with [Optimizely Data Platform (ODP)](https://docs.developers.optimizely.com/optimizely-data-platform/docs) +([#431](https://github.com/optimizely/android-sdk/pull/431), +[#440](https://github.com/optimizely/android-sdk/pull/440), +[#444](https://github.com/optimizely/android-sdk/pull/444), +[#445](https://github.com/optimizely/android-sdk/pull/445), +[#448](https://github.com/optimizely/android-sdk/pull/448)). + +You can use ODP, a high-performance [Customer Data Platform (CDP)]( https://www.optimizely.com/optimization-glossary/customer-data-platform/), to easily create complex real-time segments (RTS) using first-party and 50+ third-party data sources out of the box. You can create custom schemas that support the user attributes important for your business, and stitch together user behavior done on different devices to better understand and target your customers for personalized user experiences. ODP can be used as a single source of truth for these segments in any Optimizely or 3rd party tool. + +With ODP accounts integrated into Optimizely projects, you can build audiences using segments pre-defined in ODP. The SDK will fetch the segments for given users and make decisions using the segments. For access to ODP audience targeting in your Feature Experimentation account, please contact your Customer Success Manager. + +This version includes the following changes: + +* New API added to `OptimizelyUserContext`: + - `fetchQualifiedSegments()`: this API will retrieve user segments from the ODP server. The fetched segments will be used for audience evaluation. The fetched data will be stored in the local cache to avoid repeated network delays. + - When an `OptimizelyUserContext` is created, the SDK will automatically send an identify request to the ODP server to facilitate observing user activities. + +* New APIs added to `OptimizelyClient`: + - `sendODPEvent()`: customers can build/send arbitrary ODP events that will bind user identifiers and data to user profiles in ODP. + - `createUserContext()` with anonymous user IDs: user-contexts can be created without a userId. The SDK will create and use a persistent `VUID` specific to a device when userId is not provided. + +For details, refer to our documentation pages: + +* [Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizely-data-platform-advanced-audience-targeting) + +* [Client SDK Support](https://docs.developers.optimizely.com/feature-experimentation/v1.0/docs/advanced-audience-targeting-for-client-side-sdks) + +* [Initialize Android SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/initialize-sdk-android) + +* [OptimizelyUserContext Android SDK](https://docs.developers.optimizely.com/feature-experimentation/docs/optimizelyusercontext-android) + +* [Advanced Audience Targeting segment qualification methods](https://docs.developers.optimizely.com/feature-experimentation/docs/advanced-audience-targeting-segment-qualification-methods-android) + +* [Send Optimizely Data Platform data using Advanced Audience Targeting](https://docs.developers.optimizely.com/feature-experimentation/docs/send-odp-data-using-advanced-audience-targeting-android) + +### Breaking Changes + +* `ODPManager` in the SDK is enabled by default. Unless an ODP account is integrated into the Optimizely projects, most `ODPManager` functions will be ignored. If needed, `ODPManager` can be disabled when `OptimizelyClient` is instantiated. +* minimum Android API level requirements upgraded to 21 or higher. + + + ## 3.13.4 March 16th, 2023 @@ -150,7 +301,7 @@ Using Java SDK 3.7.0 September 30th, 2020 - This build has support for audience evaluation by version. It also supports number 'greater than or equal to' and 'less than or equal to'. -- This build also supports getting the current config string. +- This build also supports getting the current config string. For a complete list see the [release notes](https://github.com/optimizely/java-sdk/releases/tag/3.6.0) for java sdk 3.6.0 @@ -159,7 +310,7 @@ Using Java SDK 3.6.0 ## 3.6.0 July 13th, 2020 -This build has support for Feature JSON. It also includes an update to the test application +This build has support for Feature JSON. It also includes an update to the test application with feature flag example and package level log setting. Using Java SDK 3.5.0 @@ -287,10 +438,10 @@ This minor release updates the SDK to use the Optimizely Java SDK 3.1.0 which in ## 3.0.1 April 23, 2019 -This patch release fixes some git hub issues mentioned below. +This patch release fixes some git hub issues mentioned below. ### Bug Fixes -* The Logger security exception is handled a little more cleanly for logging. ([#270](https://github.com/optimizely/android-sdk/pull/270)) +* The Logger security exception is handled a little more cleanly for logging. ([#270](https://github.com/optimizely/android-sdk/pull/270)) * There was the possibility to start too many intents for event handling. ([#268](https://github.com/optimizely/android-sdk/pull/268)) * The proguard rules have been cleaned up and tested. ([#266](https://github.com/optimizely/android-sdk/pull/266)) * This also includes using Optimizely Java SDK 3.0.1. The Java SDK patch allows for using the Optimizely Android aar with older versions of org.json which are included in the android framework. @@ -356,7 +507,7 @@ This is the release candidate for the 3.0 SDK, which includes a number of improv ### New Features * Support for number-valued and boolean-valued attributes. ([#213](https://github.com/optimizely/java-sdk/pull/213)) -* Support for audiences with new match conditions for attribute values, including “substring” and “exists” matches for strings; “greater than”, “less than”, exact, and “exists” matches for numbers; and “exact”, and “exists” matches for booleans. +* Support for audiences with new match conditions for attribute values, including “substring” and “exists” matches for strings; “greater than”, “less than”, exact, and “exists” matches for numbers; and “exact”, and “exists” matches for booleans. * Built-in datafile version compatibility checks so that SDKs will not initialize with a newer datafile it is not compatible with. ([#209](https://github.com/optimizely/java-sdk/pull/209)) * Audience combinations within an experiment are unofficially supported in this release. @@ -370,7 +521,7 @@ This is the release candidate for the 3.0 SDK, which includes a number of improv * fix for exact match when dealing with integers and doubles. Created a new Numeric match type. * make a copy of attributes passed in to avoid any concurrency problems. Addresses GitHub issue in Optimizely Andriod SDK. * allow single root node for audience.conditions, typedAudience.conditions, and Experiment.audienceCombinations. - + ## 3.0.0-RC November 9, 2018 @@ -378,10 +529,10 @@ This is a RC candidate for major release 3.0.0 with support of new audience matc ### New Features * Support for number-valued and boolean-valued attributes. ([#213](https://githu b.com/optimizely/java-sdk/pull/213)) -* Support for audiences with new match conditions for attribute values, including “substring” and “exists” matches for strings; “greater than”, “less than”, exact, and “exists” matches for numbers; and “exact”, and “exists” matches for booleans. +* Support for audiences with new match conditions for attribute values, including “substring” and “exists” matches for strings; “greater than”, “less than”, exact, and “exists” matches for numbers; and “exact”, and “exists” matches for booleans. * Built-in datafile version compatibility checks so that SDKs will not initialize with a newer datafile it is not compatible with. ([#209](https://github.com/op timizely/java-sdk/pull/209)) -* Audience combinations within an experiment are unofficially supported in this +* Audience combinations within an experiment are unofficially supported in this release. ### Breaking Changes @@ -414,7 +565,7 @@ Update credits ## 2.1.0 August 2nd, 2018 -This release is the 2.x general availability launch of the Android SDK, which includes a number of significant new features that are now stable and fully supported. [Feature Management](https://developers.optimizely.com/x/solutions/sdks/reference/?language=android#feature-introduction) is now generally available, which introduces new APIs and which replaces the SDK's variable APIs (`getVariableBoolean`, etc.) with the feature variable APIs (`getFeatureVariableBoolean`, etc.). +This release is the 2.x general availability launch of the Android SDK, which includes a number of significant new features that are now stable and fully supported. [Feature Management](https://developers.optimizely.com/x/solutions/sdks/reference/?language=android#feature-introduction) is now generally available, which introduces new APIs and which replaces the SDK's variable APIs (`getVariableBoolean`, etc.) with the feature variable APIs (`getFeatureVariableBoolean`, etc.). The primary difference between the new Feature Variable APIs and the older, Variable APIs is that they allow you to link your variables to a Feature (a new type of entity defined in the Optimizely UI) and to a feature flag in your application. This in turn allows you to run Feature Tests and Rollouts on both your Features and Feature Variables. For complete details of the Feature Management APIs, see the "New Features" section below. @@ -453,7 +604,7 @@ optimizelyManager.initialize(this, new OptimizelyStartListener() { ``` ### Deprecations -* Version 2.1.0 deprecates the Variable APIs: `getVariableBoolean`, `getVariableFloat`, `getVariableInteger`, and `getVariableString` +* Version 2.1.0 deprecates the Variable APIs: `getVariableBoolean`, `getVariableFloat`, `getVariableInteger`, and `getVariableString` * Replace use of the Variable APIs with Feature Management's Feature Variable APIs, described above @@ -568,7 +719,7 @@ April 25, 2018 - Release 1.6.1 -This is a patch release for 1.6.0 and 1.5.1 Optimizely SDKs. +This is a patch release for 1.6.0 and 1.5.1 Optimizely SDKs. ### Bug Fixes * Fix for the following issue: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 856bf241..51e2b104 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,7 @@ We welcome contributions and feedback! All contributors must sign our [Contribut 7. Open a pull request from `YOUR_NAME/branch_name` to `master`. 8. A repository maintainer will review your pull request and, if all goes well, squash and merge it! -All branches will be built and run against the entire test suite on Travis with every commit. +All branches will be built and run against the entire test suite on Gtihub Actions with every commit. The `test-app` module is built against a real Optimizely project. Changing the project ID will cause tests to fail. The test app should be used as a reference. diff --git a/README.md b/README.md index 82cb1e6b..b5b7daf2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Optimizely Android SDK [![Apache 2.0](https://img.shields.io/github/license/nebula-plugins/gradle-extra-configurations-plugin.svg)](http://www.apache.org/licenses/LICENSE-2.0) -[![Build Status](https://travis-ci.org/optimizely/android-sdk.svg?branch=master)](https://travis-ci.org/optimizely/android-sdk) This repository houses the Android SDK for use with Optimizely Feature Experimentation and Optimizely Full Stack (legacy). The Android SDK depends on the [Optimizely Java SDK](https://github.com/optimizely/java-sdk). @@ -33,7 +32,7 @@ repositories { } dependencies { - implementation 'com.optimizely.ab:android-sdk:3.13.4' + implementation 'com.optimizely.ab:android-sdk:5.0.1' } ``` @@ -45,12 +44,12 @@ OptimizelyManager optimizelyManager = OptimizelyManager.builder() .withSDKKey("my_sdk_key") .withDatafileDownloadInterval(TimeUnit.MINUTES.toSeconds(15)) .build(getApplicationContext()); - + optimizelyManager.initialize(this, null, (OptimizelyClient optimizely) -> { OptimizelyClient optimizely = optimizelyManager.getOptimizely(); - + Variation variation = optimizely.activate("background_experiment", userId); - + optimizely.track("sample_conversion", userId); }); @@ -104,11 +103,11 @@ This project includes 5 library modules and a test app. ### Android Studio -Android Studio is an IDE that wraps gradle. Everything you can do in Android Studio can be done from the command line tools. +Android Studio is an IDE that wraps gradle. Everything you can do in Android Studio can be done from the command line tools. You can import this project into Android Studio by opening Android Studio and selecting `Import Project` from the first dialog or from the `File` menu. Simply select the project's root `build.gradle` file and Android Studio will do the rest. -Tests can be run by right clicking the file in the project pane or by clicking the method name in source and selecting run. You will be prompted to create an AVD or connect a device if one isn't connected. +Tests can be run by right clicking the file in the project pane or by clicking the method name in source and selecting run. You will be prompted to create an AVD or connect a device if one isn't connected. ### Contributing @@ -152,3 +151,4 @@ License (Public Domain): [https://github.com/noveogroup/android-logger/blob/mast - Ruby - https://github.com/optimizely/ruby-sdk - Swift - https://github.com/optimizely/swift-sdk + diff --git a/android-sdk/build.gradle b/android-sdk/build.gradle index 845746bf..e1f343b5 100644 --- a/android-sdk/build.gradle +++ b/android-sdk/build.gradle @@ -34,6 +34,15 @@ android { } testOptions { unitTests.returnDefaultValues = true + unitTests.all { + jvmArgs = [ + "--add-opens","java.base/java.lang.reflect=ALL-UNNAMED", + "--add-opens","java.base/java.util.concurrent=ALL-UNNAMED", + "--add-opens","java.base/java.util.concurrent.locks=ALL-UNNAMED", + "--add-opens","java.base/java.util=ALL-UNNAMED", + "--add-opens","java.base/java.lang=ALL-UNNAMED" + ] + } } buildTypes { release { @@ -45,8 +54,8 @@ android { } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } @@ -60,11 +69,14 @@ dependencies { exclude group: 'com.google.code.findbugs' } + implementation "org.slf4j:slf4j-api:$slf4j_ver" + compileOnly "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" implementation "androidx.annotation:annotation:$annotations_ver" testImplementation "junit:junit:$junit_ver" - testImplementation "org.mockito:mockito-core:$mockito_ver" + testImplementation "org.mockito:mockito-core:$mockito_ver_sdk_module" + testImplementation "org.powermock:powermock-module-junit4:$powermock_ver" testImplementation "org.powermock:powermock-api-mockito2:$powermock_ver" testImplementation "com.noveogroup.android:android-logger:$android_logger_ver" @@ -78,9 +90,7 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.google.code.gson:gson:$gson_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationTest.java index 00dd811e..9c3effda 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationTest.java @@ -42,9 +42,9 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationUpdateConfigTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationUpdateConfigTest.java index 4685b5aa..20c0b935 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationUpdateConfigTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/ODPIntegrationUpdateConfigTest.java @@ -51,9 +51,9 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; @@ -117,7 +117,9 @@ public void setup() throws Exception { notificationCenter, null, odpManager, - "test-vuid"); + "test-vuid", + null, + null); } @Test diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientEngineTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientEngineTest.java index 8dc58f94..b539fe7a 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientEngineTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientEngineTest.java @@ -35,20 +35,20 @@ @RunWith(AndroidJUnit4.class) public class OptimizelyClientEngineTest { @Test - public void testGetClientEngineFromContextAndroidTV() { + public void testGetClientEngineNameFromContextAndroidTV() { Context context = mock(Context.class); UiModeManager uiModeManager = mock(UiModeManager.class); when(context.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(uiModeManager); when(uiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_TELEVISION); - assertEquals(EventBatch.ClientEngine.ANDROID_TV_SDK, OptimizelyClientEngine.getClientEngineFromContext(context)); + assertEquals("android-tv-sdk", OptimizelyClientEngine.getClientEngineNameFromContext(context)); } @Test - public void testGetClientEngineFromContextAndroid() { + public void testGetClientEngineNameFromContextAndroid() { Context context = mock(Context.class); UiModeManager uiModeManager = mock(UiModeManager.class); when(context.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(uiModeManager); when(uiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL); - assertEquals(EventBatch.ClientEngine.ANDROID_SDK, OptimizelyClientEngine.getClientEngineFromContext(context)); + assertEquals("android-sdk", OptimizelyClientEngine.getClientEngineNameFromContext(context)); } } diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java index 3895d7e6..b7f2f7b4 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java @@ -77,12 +77,12 @@ import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertThat; import static org.junit.Assume.assumeTrue; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @RunWith(Parameterized.class) @@ -131,7 +131,7 @@ public OptimizelyClientTest(int datafileVersion,String datafile){ optimizely = Optimizely.builder(datafile, eventHandler).build(); // set to return DecisionResponse with null variation by default (instead of null DecisionResponse) - when(bucketer.bucket(anyObject(), anyObject(), anyObject())).thenReturn(DecisionResponse.nullNoReasons()); + when(bucketer.bucket(any(), any(), any())).thenReturn(DecisionResponse.nullNoReasons()); if(datafileVersion==3) { Variation variation = optimizely.getProjectConfig().getExperiments().get(0).getVariations().get(0); @@ -431,7 +431,7 @@ public void testGoodForcedTrack() { optimizelyClient.track("test_event", GENERIC_USER_ID); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); ArgumentCaptor<LogEvent> logEventArgumentCaptor = ArgumentCaptor.forClass(LogEvent.class); try { @@ -462,7 +462,7 @@ public void testGoodTrack() { OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger); optimizelyClient.track("test_event", GENERIC_USER_ID); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -484,7 +484,7 @@ public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull M assertTrue(notificationId <= 0); assertFalse(optimizelyClient.getNotificationCenter().removeNotificationListener(notificationId)); assertEquals(false, numberOfCalls[0]); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @@ -512,7 +512,7 @@ public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull M else { assertEquals(true, numberOfCalls[0]); } - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @@ -524,7 +524,7 @@ public void testGoodTrackBucketing() { Experiment experiment = optimizelyClient.getProjectConfig().getExperimentsForEventKey("test_event").get(0); attributes.put(BUCKETING_ATTRIBUTE, bucketingId); optimizelyClient.track("test_event", "userId", attributes); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -569,7 +569,7 @@ public void testGoodForcedTrackAttribute() { optimizelyClient.track("test_event", GENERIC_USER_ID, attributes); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); ArgumentCaptor<LogEvent> logEventArgumentCaptor = ArgumentCaptor.forClass(LogEvent.class); @@ -611,7 +611,7 @@ public void testGoodTrackAttribute() { optimizelyClient.track("test_event", GENERIC_USER_ID, attributes); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); Variation v = optimizelyClient.getForcedVariation(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID); assertEquals(v.getKey(), "var_2"); @@ -671,7 +671,7 @@ public void testGoodForcedTrackEventVal() { Collections.<String, String>emptyMap(), Collections.singletonMap(ReservedEventKey.REVENUE.toString(), 1L)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); ArgumentCaptor<LogEvent> logEventArgumentCaptor = ArgumentCaptor.forClass(LogEvent.class); @@ -705,7 +705,7 @@ public void testGoodTrackEventVal() { GENERIC_USER_ID, Collections.<String, String>emptyMap(), Collections.singletonMap(ReservedEventKey.REVENUE.toString(), 1L)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -750,7 +750,7 @@ public void testGoodTrackAttributeEventVal() { final HashMap<String, String> attributes = new HashMap<>(); optimizelyClient.track("test_event", GENERIC_USER_ID, attributes, Collections.singletonMap(ReservedEventKey.REVENUE.toString(), 1L)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -770,7 +770,7 @@ public void testGoodForcedTrackAttributeEventVal() { attributes, Collections.singletonMap(ReservedEventKey.REVENUE.toString(), 1L)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); ArgumentCaptor<LogEvent> logEventArgumentCaptor = ArgumentCaptor.forClass(LogEvent.class); @@ -839,7 +839,7 @@ public void testTrackWithEventTags() { final HashMap<String, Object> eventTags = new HashMap<>(); eventTags.put("foo", 843); optimizelyClient.track("test_event", GENERIC_USER_ID, attributes, eventTags); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -875,7 +875,7 @@ public void testForcedTrackWithEventTags() { // id of var_2 assertTrue(logEvent.getBody().contains("\"enrich_decisions\":true")); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); Variation v = optimizelyClient.getForcedVariation(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID); assertEquals(v.getKey(), "var_2"); @@ -979,7 +979,7 @@ public void testGoodGetVariationAttribute() { logger); final HashMap<String, String> attributes = new HashMap<>(); optimizelyClient.getVariation(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID, attributes); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @Test @@ -996,7 +996,7 @@ public void testGoodForcedGetVariationAttribute() { v = optimizelyClient.getVariation(FEATURE_ANDROID_EXPERIMENT_KEY, GENERIC_USER_ID, attributes); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); assertEquals(v.getKey(), "var_2"); @@ -1180,7 +1180,7 @@ public void testGoodIsFeatureEnabledWithAttribute() { Collections.singletonMap("house", "Gryffindor") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); assertFalse(optimizelyClient.isFeatureEnabled( "InvalidFeatureKey", @@ -1301,7 +1301,7 @@ public void testIsFeatureEnabledWithFeatureEnabledTrue(){ Collections.singletonMap("house", "Gryffindor") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @@ -1398,7 +1398,7 @@ public void testGoodGetFeatureVariableBooleanWithAttr() { GENERIC_USER_ID, Collections.singletonMap("key", "value") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } @@ -1505,7 +1505,7 @@ public void testGoodGetFeatureVariableDoubleWithAttr() { GENERIC_USER_ID, Collections.singletonMap("house", "Gryffindor") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } //FeatureVariableDouble Scenario#3 if feature not found @@ -1616,7 +1616,7 @@ public void testGoodGetFeatureVariableIntegerWithAttr() { GENERIC_USER_ID, Collections.singletonMap("house", "Gryffindor") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } //FeatureVariableInteger Scenario#3 if feature not found @@ -1723,7 +1723,7 @@ public void testGoodGetFeatureVariableStringWithAttr() { GENERIC_USER_ID, Collections.singletonMap("house", "Gryffindor") )); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } //FeatureVariableString Scenario#3 if feature not found @@ -1837,7 +1837,7 @@ public void testGetFeatureVariableJsonWithAttr() { ); assertTrue(compareJsonStrings(json.toString(), defaultValueOfStringVar)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } //FeatureVariableJSON Scenario#3 if feature not found @@ -1949,7 +1949,7 @@ public void testGetAllFeatureVariablesWithAttr() { ); assertTrue(compareJsonStrings(json.toString(), defaultValueOfStringVar)); - verifyZeroInteractions(logger); + verifyNoInteractions(logger); } //GetAllFeatureVariables Scenario#3 if feature not found diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyDefaultAttributesTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyDefaultAttributesTest.java index f730f3d6..930cfcc8 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyDefaultAttributesTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyDefaultAttributesTest.java @@ -26,8 +26,8 @@ import org.junit.runner.RunWith; import org.slf4j.Logger; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerEventHandlerTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerEventHandlerTest.java index 9bfae9c9..a6a4f372 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerEventHandlerTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerEventHandlerTest.java @@ -65,4 +65,25 @@ public void eventClientNameAndVersion() throws Exception { assertEquals(argument.getValue().getEventBatch().getClientVersion(), BuildConfig.CLIENT_VERSION); } + @Test + public void eventClientWithCustomNameAndVersion() throws Exception { + EventHandler mockEventHandler = mock(EventHandler.class); + + Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); + OptimizelyManager optimizelyManager = OptimizelyManager.builder() + .withSDKKey("any-sdk-key") + .withEventDispatchInterval(0, TimeUnit.SECONDS) + .withEventHandler(mockEventHandler) + .withClientInfo("test-sdk", "test-version") + .build(context); + + OptimizelyClient optimizelyClient = optimizelyManager.initialize(context, minDatafileWithEvent); + optimizelyClient.track("test_event", "tester"); + + ArgumentCaptor<LogEvent> argument = ArgumentCaptor.forClass(LogEvent.class); + verify(mockEventHandler, timeout(5000)).dispatchEvent(argument.capture()); + assertEquals(argument.getValue().getEventBatch().getClientName(), "test-sdk"); + assertEquals(argument.getValue().getEventBatch().getClientVersion(), "test-version"); + } + } diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java index 3052612e..4e44808e 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java @@ -21,6 +21,7 @@ import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.util.Log; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SdkSuppress; @@ -48,6 +49,8 @@ import org.mockito.stubbing.Answer; import org.slf4j.Logger; +import java.util.Map; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -58,13 +61,10 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.doAnswer; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; +import static org.junit.Assert.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; /** * Tests for {@link OptimizelyManager} @@ -163,7 +163,7 @@ public void initializeSyncWithEnvironment() { EventHandler eventHandler = mock(DefaultEventHandler.class); EventProcessor eventProcessor = mock(EventProcessor.class); OptimizelyManager optimizelyManager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, 3600L, datafileHandler, null, 3600L, - eventHandler, eventProcessor, null, null, null, null, null); + eventHandler, eventProcessor, null, null, null, null, null, null, null); /* * Scenario#1: when datafile is not Empty * Scenario#2: when datafile is Empty @@ -222,7 +222,7 @@ public void initializeAsyncWithEnvironment() { EventHandler eventHandler = mock(DefaultEventHandler.class); EventProcessor eventProcessor = mock(EventProcessor.class); final OptimizelyManager optimizelyManager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, 3600L, datafileHandler, null, 3600L, - eventHandler, eventProcessor, null, null, null, null, null); + eventHandler, eventProcessor, null, null, null, null, null, null, null); /* * Scenario#1: when datafile is not Empty @@ -359,7 +359,7 @@ public void injectOptimizely() { UserProfileService userProfileService = mock(UserProfileService.class); OptimizelyStartListener startListener = mock(OptimizelyStartListener.class); - optimizelyManager.setOptimizelyStartListener(startListener); + optimizelyManager.setOptimizelyStartListener(startListener, true); optimizelyManager.injectOptimizely(context, userProfileService, minDatafile); try { executor.awaitTermination(5, TimeUnit.SECONDS); @@ -494,16 +494,22 @@ public void initializeSyncWithUpdateOnNewDatafileDisabled() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -527,16 +533,22 @@ public void initializeSyncWithUpdateOnNewDatafileEnabled() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -560,16 +572,22 @@ public void initializeSyncWithDownloadToCacheDisabled() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -593,14 +611,22 @@ public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingEnab Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - (Answer<Object>) invocation -> { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -625,16 +651,22 @@ public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingEnabl Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -658,17 +690,23 @@ public void initializeSyncWithUpdateOnNewDatafileDisabledWithPeriodicPollingDisa Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); + OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); try { @@ -692,16 +730,22 @@ public void initializeSyncWithUpdateOnNewDatafileEnabledWithPeriodicPollingDisab Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null); + null, null, null, null, null, null, null, null, null); - doAnswer( - new Answer<Object>() { - public Object answer(InvocationOnMock invocation) { - String newDatafile = manager.getDatafile(context, R.raw.datafile_api); - datafileHandler.saveDatafile(context, manager.getDatafileConfig(), newDatafile); - return null; - } - }).when(manager.getDatafileHandler()).downloadDatafile(any(Context.class), any(DatafileConfig.class), any(DatafileLoadedListener.class)); + ArgumentCaptor<Context> contextCaptor = ArgumentCaptor.forClass(Context.class); + ArgumentCaptor<DatafileConfig> configCaptor = ArgumentCaptor.forClass(DatafileConfig.class); + ArgumentCaptor<DatafileLoadedListener> listenerCaptor = ArgumentCaptor.forClass(DatafileLoadedListener.class); + + doAnswer(invocation -> { + Context capturedContext = contextCaptor.getValue(); + DatafileConfig capturedConfig = configCaptor.getValue(); + DatafileLoadedListener capturedListener = listenerCaptor.getValue(); + + String newDatafile = manager.getDatafile(capturedContext, R.raw.datafile_api); + datafileHandler.saveDatafile(capturedContext, capturedConfig, newDatafile); + + return datafileHandler; + }).when(manager.getDatafileHandler()).downloadDatafile(contextCaptor.capture(), configCaptor.capture(), listenerCaptor.capture()); OptimizelyClient client = manager.initialize(context, defaultDatafile, downloadToCache, updateConfigOnNewDatafile); @@ -725,7 +769,7 @@ public void initializeSyncWithResourceDatafileNoCache() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null)); datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig()); OptimizelyClient client = manager.initialize(context, R.raw.datafile, downloadToCache, updateConfigOnNewDatafile); @@ -742,7 +786,7 @@ public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() { Context context = InstrumentationRegistry.getInstrumentation().getTargetContext(); OptimizelyManager manager = spy(new OptimizelyManager(testProjectId, testSdkKey, null, logger, pollingInterval, datafileHandler, null, 0, - null, null, null, null, null, null, null)); + null, null, null, null, null, null, null, null, null)); datafileHandler.removeSavedDatafile(context, manager.getDatafileConfig()); OptimizelyClient client = manager.initialize(context, R.raw.datafile); @@ -750,6 +794,72 @@ public void initializeSyncWithResourceDatafileNoCacheWithDefaultParams() { verify(manager).initialize(eq(context), eq(defaultDatafile), eq(true), eq(false)); } + @Test + public void initializeAsyncCallbackInBackgroundThread() throws InterruptedException { + OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId) + .build(InstrumentationRegistry.getInstrumentation().getTargetContext()); + + CountDownLatch latch = new CountDownLatch(1); + + // by default, async init returns in main thread. + // this parameter should be set to false to overrule it. + boolean returnInMainThread = false; + + optimizelyManager.initialize( + InstrumentationRegistry.getInstrumentation().getContext(), + null, + returnInMainThread, + (client) -> { + Log.d("Optly", "[TESTING] " + Thread.currentThread().getName()); + try { + assertNotEquals( + "OptimizelyStartListener should be called in a background thread", + "main", Thread.currentThread().getName() + ); + latch.countDown(); + } catch (AssertionError e) { + // we need catch and silence this assertion error, otherwise it will be caught in OptimizeManager, + // and give a wrong error message. The failure will be detected with the latch timeout below. + } + } + ); + + boolean completed = latch.await(1, TimeUnit.SECONDS); + if (!completed) { + fail("OptimizelyStartListener thread checking failed"); + } + } + + @Test + public void initializeAsyncCallbackInMainThread() throws InterruptedException { + OptimizelyManager optimizelyManager = OptimizelyManager.builder(testProjectId) + .build(InstrumentationRegistry.getInstrumentation().getTargetContext()); + + CountDownLatch latch = new CountDownLatch(1); + + optimizelyManager.initialize( + InstrumentationRegistry.getInstrumentation().getContext(), + null, + (client) -> { + Log.d("Optly", "[TESTING] " + Thread.currentThread().getName()); + try { + assertEquals( + "OptimizelyStartListener should be called in a background thread", + "main", Thread.currentThread().getName() + ); + latch.countDown(); + } catch (AssertionError e) { + // we need catch and silence this assertion error, otherwise it will be caught in OptimizeManager, + // and give a wrong error message. The failure will be detected with the latch timeout below. + } + } + ); + + boolean completed = latch.await(1, TimeUnit.SECONDS); + if (!completed) { + fail("OptimizelyStartListener thread checking failed"); + } + } // Utils diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java index 2332868d..8c3a6265 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClient.java @@ -80,7 +80,7 @@ public class OptimizelyClient { So, we start with an empty map of default attributes until the manager is initialized. */ - if (isValid()) { + if (isValid() && vuid != null) { // identifiers are empty here since vuid will be inserted by java-sdk core sendODPEvent(null, "client_initialized", null, null); } diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClientEngine.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClientEngine.java index 485917ae..b0f3c5f0 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClientEngine.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClientEngine.java @@ -28,12 +28,30 @@ */ public class OptimizelyClientEngine { + /** + * Get client engine name for current UI mode type + * + * @param context any valid Android {@link Context} + * @return client engine name ("android-sdk" or "android-tv-sdk") + */ + public static String getClientEngineNameFromContext(@NonNull Context context) { + UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); + + if (uiModeManager != null && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { + return "android-tv-sdk"; + } + + return "android-sdk"; + } + /** * Get client engine value for current UI mode type * * @param context any valid Android {@link Context} * @return String value of client engine + * @deprecated Consider using {@link #getClientEngineNameFromContext(Context)} */ + @Deprecated public static EventBatch.ClientEngine getClientEngineFromContext(@NonNull Context context) { UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE); diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java index 6ca77548..ac43c8e6 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java @@ -48,11 +48,7 @@ import com.optimizely.ab.event.BatchEventProcessor; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; -import com.optimizely.ab.event.internal.BuildVersionInfo; -import com.optimizely.ab.event.internal.ClientEngineInfo; -import com.optimizely.ab.event.internal.payload.EventBatch; import com.optimizely.ab.notification.NotificationCenter; -import com.optimizely.ab.notification.UpdateConfigNotification; import com.optimizely.ab.odp.ODPApiManager; import com.optimizely.ab.odp.ODPEventManager; import com.optimizely.ab.odp.ODPManager; @@ -65,7 +61,6 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -97,9 +92,11 @@ public class OptimizelyManager { @Nullable private final String vuid; @Nullable private OptimizelyStartListener optimizelyStartListener; + private boolean returnInMainThreadFromAsyncInit = true; @Nullable private final List<OptimizelyDecideOption> defaultDecideOptions; - private String sdkVersion = null; + private String customSdkName = null; + private String customSdkVersion = null; OptimizelyManager(@Nullable String projectId, @Nullable String sdkKey, @@ -115,7 +112,9 @@ public class OptimizelyManager { @NonNull NotificationCenter notificationCenter, @Nullable List<OptimizelyDecideOption> defaultDecideOptions, @Nullable ODPManager odpManager, - @Nullable String vuid) { + @Nullable String vuid, + @Nullable String clientEngineName, + @Nullable String clientVersion) { if (projectId == null && sdkKey == null) { logger.error("projectId and sdkKey are both null!"); @@ -141,12 +140,8 @@ public class OptimizelyManager { this.notificationCenter = notificationCenter; this.defaultDecideOptions = defaultDecideOptions; - try { - sdkVersion = BuildConfig.CLIENT_VERSION; - logger.info("SDK Version: {}", sdkVersion); - } catch (Exception e) { - logger.warn("Error getting BuildConfig version"); - } + this.customSdkName = clientEngineName; + this.customSdkVersion = clientVersion; } @VisibleForTesting @@ -175,8 +170,14 @@ OptimizelyStartListener getOptimizelyStartListener() { return optimizelyStartListener; } - void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener) { + void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener, boolean returnInMainThread) { this.optimizelyStartListener = optimizelyStartListener; + this.returnInMainThreadFromAsyncInit = returnInMainThread; + } + + void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStartListener) { + boolean returnInMainThread = true; + setOptimizelyStartListener(optimizelyStartListener, returnInMainThread); } private void notifyStartListener() { @@ -364,7 +365,7 @@ public String getDatafile(Context context,@RawRes Integer datafileRes){ } } return safeLoadResource(context, datafileRes); - } catch (NullPointerException e){ + } catch (NullPointerException | Resources.NotFoundException e){ logger.error("Unable to find compiled data file in raw resource",e); } return null; @@ -398,11 +399,27 @@ public void initialize(@NonNull final Context context, @NonNull OptimizelyStartL * @see #initialize(Context, Integer, OptimizelyStartListener) */ @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) - public void initialize(@NonNull final Context context, @RawRes final Integer datafileRes, @NonNull OptimizelyStartListener optimizelyStartListener) { + public void initialize( + @NonNull final Context context, + @RawRes final Integer datafileRes, + @NonNull OptimizelyStartListener optimizelyStartListener) + { + // return in main thread after async completed (backward compatible) + boolean returnInMainThread = true; + initialize(context, datafileRes, returnInMainThread, optimizelyStartListener); + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + public void initialize( + @NonNull final Context context, + @RawRes final Integer datafileRes, + final boolean returnInMainThread, + @NonNull OptimizelyStartListener optimizelyStartListener) + { if (!isAndroidVersionSupported()) { return; } - setOptimizelyStartListener(optimizelyStartListener); + setOptimizelyStartListener(optimizelyStartListener, returnInMainThread); datafileHandler.downloadDatafile(context, datafileConfig, getDatafileLoadedListener(context,datafileRes)); } @@ -514,6 +531,29 @@ public DatafileHandler getDatafileHandler() { return datafileHandler; } + @NonNull + public String getSdkName(Context context) { + String sdkName = customSdkName; + if (sdkName == null) { + sdkName = OptimizelyClientEngine.getClientEngineNameFromContext(context); + } + return sdkName; + } + + @NonNull + public String getSdkVersion() { + String sdkVersion = customSdkVersion; + if (sdkVersion == null) { + try { + sdkVersion = BuildConfig.CLIENT_VERSION; + } catch (Exception e) { + logger.warn("Error getting BuildConfig version"); + sdkVersion = "UNKNOWN"; + } + } + return sdkVersion; + } + private boolean datafileDownloadEnabled() { return datafileDownloadInterval > 0; } @@ -553,7 +593,7 @@ public void onStartComplete(UserProfileService userProfileService) { logger.info("No listener to send Optimizely to"); } } - }); + }, returnInMainThreadFromAsyncInit); } else { if (optimizelyStartListener != null) { @@ -577,7 +617,8 @@ public void onStartComplete(UserProfileService userProfileService) { private OptimizelyClient buildOptimizely(@NonNull Context context, @NonNull String datafile) throws ConfigParseException { EventHandler eventHandler = getEventHandler(context); - EventBatch.ClientEngine clientEngine = OptimizelyClientEngine.getClientEngineFromContext(context); + String sdkName = getSdkName(context); + String sdkVersion = getSdkVersion(); Optimizely.Builder builder = Optimizely.builder(); @@ -594,7 +635,8 @@ private OptimizelyClient buildOptimizely(@NonNull Context context, @NonNull Stri } // override client sdk name/version to be included in events - builder.withClientInfo(clientEngine, sdkVersion); + builder.withClientInfo(sdkName, sdkVersion); + logger.info("SDK name: {} and version: {}", sdkName, sdkVersion); if (errorHandler != null) { builder.withErrorHandler(errorHandler); @@ -745,8 +787,12 @@ public static class Builder { private int timeoutForODPSegmentFetchInSecs = 10; private int timeoutForODPEventDispatchInSecs = 10; private boolean odpEnabled = true; + private boolean vuidEnabled = false; private String vuid = null; + private String customSdkName = null; + private String customSdkVersion = null; + @Deprecated /** * @deprecated use {@link #Builder()} instead and pass in an SDK Key with {@link #withSDKKey(String)} @@ -981,6 +1027,15 @@ public Builder withODPDisabled() { return this; } + /** + * Enable Vuid + * @return this {@link Builder} instance + */ + public Builder withVuidEnabled() { + this.vuidEnabled = true; + return this; + } + /** * Override the default (SDK-generated and persistent) vuid. * @param vuid a user-defined vuid value @@ -991,6 +1046,18 @@ public Builder withVuid(String vuid) { return this; } + /** + * Override the SDK name and version (for client SDKs like flutter-sdk wrapping the core android-sdk) to be included in events. + * + * @param clientEngineName the client engine name ("flutter/android-sdk", etc.). + * @param clientVersion the client SDK version. + * @return this {@link Builder} instance + */ + public Builder withClientInfo(@Nullable String clientEngineName, @Nullable String clientVersion) { + this.customSdkName = clientEngineName; + this.customSdkVersion = clientVersion; + return this; + } /** * Get a new {@link Builder} instance to create {@link OptimizelyManager} with. * @param context the application context used to create default service if not provided. @@ -1058,8 +1125,11 @@ public OptimizelyManager build(Context context) { } - if (vuid == null) { - vuid = VuidManager.Companion.getShared(context).getVuid(); + VuidManager vuidManager = VuidManager.Companion.getInstance(); + vuidManager.configure(vuidEnabled, context); + + if (vuid == null && vuidEnabled) { + vuid = vuidManager.getVuid(); } ODPManager odpManager = null; @@ -1103,7 +1173,10 @@ public OptimizelyManager build(Context context) { notificationCenter, defaultDecideOptions, odpManager, - vuid); + vuid, + customSdkName, + customSdkVersion + ); } } } diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java index f5df9af0..92731f5c 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyClientTest.java @@ -29,12 +29,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.mockito.exceptions.verification.junit.ArgumentsAreDifferent; import org.slf4j.Logger; -import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -43,6 +41,9 @@ import static junit.framework.Assert.assertFalse; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import org.powermock.reflect.Whitebox; + + /** * Tests for {@link OptimizelyClient} @@ -56,28 +57,9 @@ public class OptimizelyClientTest { @Before public void setup() { - Field field = null; - try { - field = Optimizely.class.getDeclaredField("notificationCenter"); - // Mark the field as public so we can toy with it - field.setAccessible(true); -// Get the Modifiers for the Fields - Field modifiersField = Field.class.getDeclaredField("modifiers"); -// Allow us to change the modifiers - modifiersField.setAccessible(true); - // Remove final modifier from field by blanking out the bit that says "FINAL" in the Modifiers - modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); -// Set new value - field.set(optimizely, notificationCenter); - - when(optimizely.isValid()).thenReturn(true); - } catch (NoSuchFieldException e) { - e.printStackTrace(); - } catch (IllegalAccessException e) { - e.printStackTrace(); - } + Whitebox.setInternalState(optimizely, "notificationCenter", notificationCenter); + when(optimizely.isValid()).thenReturn(true); } - @Test(expected=ArgumentsAreDifferent.class) public void testGoodActivation1() { OptimizelyClient optimizelyClient = new OptimizelyClient(optimizely, logger); diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java index 63eaf9c0..d6c74757 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerBuilderTest.java @@ -17,9 +17,7 @@ package com.optimizely.ab.android.sdk; import android.content.Context; -import android.graphics.Path; -import com.optimizely.ab.Optimizely; import com.optimizely.ab.android.datafile_handler.DatafileHandler; import com.optimizely.ab.android.datafile_handler.DefaultDatafileHandler; import com.optimizely.ab.android.event_handler.DefaultEventHandler; @@ -28,7 +26,6 @@ import com.optimizely.ab.android.odp.ODPSegmentClient; import com.optimizely.ab.android.odp.VuidManager; import com.optimizely.ab.android.shared.DatafileConfig; -import com.optimizely.ab.android.shared.WorkerScheduler; import com.optimizely.ab.android.user_profile.DefaultUserProfileService; import com.optimizely.ab.bucketing.UserProfileService; import com.optimizely.ab.error.ErrorHandler; @@ -36,52 +33,55 @@ import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; import com.optimizely.ab.notification.NotificationCenter; -import com.optimizely.ab.odp.ODPApiManager; import com.optimizely.ab.odp.ODPEventManager; import com.optimizely.ab.odp.ODPManager; import com.optimizely.ab.odp.ODPSegmentManager; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; -import org.mockito.runners.MockitoJUnitRunner; +import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import org.slf4j.Logger; import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.whenNew; -import java.sql.Time; import java.util.Map; import java.util.concurrent.TimeUnit; @RunWith(PowerMockRunner.class) -@PowerMockIgnore("jdk.internal.reflect.*") -@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, ODPManager.class, ODPSegmentManager.class, ODPEventManager.class}) +@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, ODPManager.class, ODPSegmentManager.class, ODPEventManager.class, VuidManager.class}) public class OptimizelyManagerBuilderTest { private String testProjectId = "7595190003"; private String testSdkKey = "1234"; private Logger logger; + private VuidManager mockVuidManager; + private String minDatafile = "{\n" + "experiments: [ ],\n" + "version: \"2\",\n" + @@ -101,6 +101,15 @@ public class OptimizelyManagerBuilderTest { public void setup() throws Exception { mockContext = mock(Context.class); mockDatafileHandler = mock(DefaultDatafileHandler.class); + + mockStatic(VuidManager.class); + VuidManager.Companion mockCompanion = PowerMockito.mock(VuidManager.Companion.class); + mockVuidManager = PowerMockito.mock(VuidManager.class); + PowerMockito.doReturn(mockVuidManager).when(mockCompanion).getInstance(); + Whitebox.setInternalState( + VuidManager.class, "Companion", + mockCompanion + ); } /** @@ -216,18 +225,14 @@ public void testBuildWithDatafileDownloadInterval_workerCancelledWhenIntervalIsN } @Test - public void testBuildWithDatafileDownloadInterval_workerCancelledWhenNoIntervalProvided() throws Exception { + public void testBuildWithCustomSdkNameAndVersion() throws Exception { OptimizelyManager manager = OptimizelyManager.builder() - .withSDKKey(testSdkKey) - .withDatafileHandler(mockDatafileHandler) - .withVuid("any-to-avoid-generate") - .build(mockContext); - OptimizelyManager spyManager = spy(manager); - when(spyManager.isAndroidVersionSupported()).thenReturn(true); - spyManager.initialize(mockContext, ""); - - verify(mockDatafileHandler).stopBackgroundUpdates(any(), any()); - verify(mockDatafileHandler, never()).startBackgroundUpdates(any(), any(), any(), any()); + .withSDKKey(testSdkKey) + .withClientInfo("test-sdk", "test-version") + .withVuid("any-to-avoid-generate") + .build(mockContext); + assertEquals(manager.getSdkName(mockContext), "test-sdk"); + assertEquals(manager.getSdkVersion(), "test-version"); } @Test @@ -254,7 +259,9 @@ public void testBuildWithDefaultODP_defaultEnabled() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - eq("test-vuid")); + eq("test-vuid"), + any(), + any()); } @Test @@ -282,7 +289,9 @@ public void testBuildWithDefaultODP_disabled() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) isNull(), - eq("test-vuid")); + eq("test-vuid"), + any(), + any()); } @Test @@ -400,4 +409,60 @@ public void testBuildWithODP_defaultCommonDataAndIdentifiers() throws Exception assertEquals(identifiers.size(), 1); } + ODPManager.Builder getMockODPManagerBuilder() { + ODPManager.Builder mockBuilder = PowerMockito.mock(ODPManager.Builder.class); + when(mockBuilder.withApiManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentCacheSize(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentCacheTimeout(any())).thenReturn(mockBuilder); + when(mockBuilder.withSegmentManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withEventManager(any())).thenReturn(mockBuilder); + when(mockBuilder.withUserCommonData(any())).thenReturn(mockBuilder); + when(mockBuilder.withUserCommonIdentifiers(any())).thenReturn(mockBuilder); + return mockBuilder; + } + + @Test + public void testBuildWithVuidDisabled() throws Exception { + mockStatic(ODPManager.class); + ODPManager.Builder mockBuilder = getMockODPManagerBuilder(); + when(mockBuilder.build()).thenReturn(mock(ODPManager.class)); + when(ODPManager.builder()).thenReturn(mockBuilder); + + OptimizelyManager manager = OptimizelyManager.builder() + .withSDKKey(testSdkKey) + .build(mockContext); + + verify(mockVuidManager, times(1)).configure(eq(false), any(Context.class)); + + ArgumentCaptor<Map<String, String>> identifiersCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockBuilder).withUserCommonIdentifiers(identifiersCaptor.capture()); + Map<String, String> identifiers = identifiersCaptor.getValue(); + assertFalse(identifiers.containsKey("vuid")); + + when(ODPManager.builder()).thenCallRealMethod(); + } + + @Test + public void testBuildWithVuidEnabled() throws Exception { + mockStatic(ODPManager.class); + ODPManager.Builder mockBuilder = getMockODPManagerBuilder(); + when(mockBuilder.build()).thenReturn(mock(ODPManager.class)); + when(ODPManager.builder()).thenReturn(mockBuilder); + + when(mockVuidManager.getVuid()).thenReturn("vuid_test"); + + OptimizelyManager manager = OptimizelyManager.builder() + .withSDKKey(testSdkKey) + .withVuidEnabled() + .build(mockContext); + + verify(mockVuidManager, times(1)).configure(eq(true), any(Context.class)); + + ArgumentCaptor<Map<String, String>> identifiersCaptor = ArgumentCaptor.forClass(Map.class); + verify(mockBuilder).withUserCommonIdentifiers(identifiersCaptor.capture()); + Map<String, String> identifiers = identifiersCaptor.getValue(); + assertEquals(identifiers.get("vuid"), "vuid_test"); + + when(ODPManager.builder()).thenCallRealMethod(); + } } diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java index b82f9857..378d4ecb 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerIntervalTest.java @@ -16,15 +16,16 @@ package com.optimizely.ab.android.sdk; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.anyList; -import static org.mockito.Matchers.anyLong; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.powermock.api.mockito.PowerMockito.doReturn; import static org.powermock.api.mockito.PowerMockito.mockStatic; import static org.powermock.api.mockito.PowerMockito.verifyNew; import static org.powermock.api.mockito.PowerMockito.whenNew; @@ -33,9 +34,9 @@ import com.optimizely.ab.android.datafile_handler.DatafileHandler; import com.optimizely.ab.android.event_handler.DefaultEventHandler; +import com.optimizely.ab.android.odp.VuidManager; import com.optimizely.ab.android.shared.DatafileConfig; import com.optimizely.ab.bucketing.UserProfileService; -import com.optimizely.ab.error.ErrorHandler; import com.optimizely.ab.event.BatchEventProcessor; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.EventProcessor; @@ -45,11 +46,12 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.powermock.core.PowerMockUtils; +import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PowerMockIgnore; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; +import org.powermock.reflect.Whitebox; import org.slf4j.Logger; import java.util.concurrent.BlockingQueue; @@ -59,7 +61,7 @@ @RunWith(PowerMockRunner.class) @PowerMockIgnore("jdk.internal.reflect.*") -@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class}) +@PrepareForTest({OptimizelyManager.class, BatchEventProcessor.class, DefaultEventHandler.class, VuidManager.class}) public class OptimizelyManagerIntervalTest { private Logger logger; @@ -76,6 +78,15 @@ public void setup() throws Exception { mockEventHandler = mock(DefaultEventHandler.class); mockStatic(DefaultEventHandler.class); when(DefaultEventHandler.getInstance(any())).thenReturn(mockEventHandler); + + mockStatic(VuidManager.class); + VuidManager.Companion mockCompanion = PowerMockito.mock(VuidManager.Companion.class); + VuidManager mockVuidManager = PowerMockito.mock(VuidManager.class); + doReturn(mockVuidManager).when(mockCompanion).getInstance(); + Whitebox.setInternalState( + VuidManager.class, "Companion", + mockCompanion + ); } // DatafileDownloadInterval @@ -104,7 +115,9 @@ public void testBuildWithDatafileDownloadInterval() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - anyString()); + anyString(), + any(), + any()); } @Test @@ -131,7 +144,9 @@ public void testBuildWithDatafileDownloadIntervalDeprecated() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - anyString()); + anyString(), + any(), + any()); } @Test @@ -170,7 +185,9 @@ public void testBuildWithEventDispatchInterval() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - anyString()); + anyString(), + any(), + any()); } @Test @@ -212,7 +229,9 @@ public void testBuildWithEventDispatchRetryInterval() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - anyString()); + anyString(), + any(), + any()); } @Test @@ -250,7 +269,9 @@ public void testBuildWithEventDispatchIntervalDeprecated() throws Exception { any(NotificationCenter.class), any(), // nullable (DefaultDecideOptions) any(ODPManager.class), - anyString()); + anyString(), + any(), + any()); } } diff --git a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java index e00e89b9..bb2264f3 100644 --- a/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java +++ b/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java @@ -23,7 +23,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import static org.mockito.Mockito.verify; diff --git a/build.gradle b/build.gradle index bbac9eae..b4d3c3f0 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { ext.kotlin_version = '1.7.0' - ext.version_name = System.getenv('TRAVIS_TAG') + ext.version_name = System.getenv('GITHUB_TAG') if (version_name == null || version_name.isEmpty()) { ext.version_name = 'debugVersion' } @@ -57,8 +57,9 @@ allprojects { mavenCentral() // SNAPSHOT support maven {url "https://oss.sonatype.org/content/repositories/snapshots/" } + maven { url "https://jitpack.io" } } - + configurations.all { // no cache for SNAPSHOT dependency resolutionStrategy.cacheChangingModulesFor 0, 'seconds' @@ -68,15 +69,15 @@ allprojects { ext { compile_sdk_version = 33 build_tools_version = "30.0.3" - min_sdk_version = 14 + min_sdk_version = 21 target_sdk_version = 33 - //java_core_ver = "3.10.2" - java_core_ver = "BB-SNAPSHOT" + java_core_ver = "4.2.2" android_logger_ver = "1.3.6" jacksonversion= "2.11.2" annotations_ver = "1.2.0" junit_ver = "4.12" - mockito_ver = "1.10.19" + mockito_ver = "4.11.0" + mockito_ver_sdk_module = "3.6.28" powermock_ver = "2.0.9" support_test_runner_ver = "0.5" dexmaker_ver = "1.4" @@ -87,6 +88,7 @@ ext { androidx_test_core = "1.4.0" androidx_test_rules = "1.4.0" espresso_ver = "3.4.0" + slf4j_ver = "1.7.3" } task clean(type: Delete) { @@ -101,11 +103,6 @@ task cleanAllModules () { task testAllModules () { logger.info("Running android tests for all modules") - dependsOn('testAllModulesTravis', ':test-app:connectedAndroidTest') -} - -task testAllModulesTravis () { - logger.info("Running android tests for Travis") dependsOn(':android-sdk:connectedAndroidTest', ':android-sdk:test', ':event-handler:connectedAndroidTest', ':event-handler:test', ':datafile-handler:connectedAndroidTest', ':datafile-handler:test', @@ -114,6 +111,10 @@ task testAllModulesTravis () { ':odp:connectedAndroidTest', ':odp:test' ) } +task testODPModule () { + logger.info("Running android tests for ODP") + dependsOn(':android-sdk:connectedAndroidTest', ':android-sdk:test',) +} // Publish to MavenCentral @@ -236,7 +237,6 @@ configure(publishedProjects) { } signing { - // base64 for workaround travis escape chars issue def signingKeyBase64 = System.getenv('MAVEN_SIGNING_KEY_BASE64') // skip signing for "local" version into MavenLocal if (!signingKeyBase64?.trim()) return diff --git a/datafile-handler/build.gradle b/datafile-handler/build.gradle index 6cdbfc68..4e4d592d 100644 --- a/datafile-handler/build.gradle +++ b/datafile-handler/build.gradle @@ -39,8 +39,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } @@ -64,9 +64,7 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/BackgroundWatchersCacheTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/BackgroundWatchersCacheTest.java index 10aa9181..9c51b8f5 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/BackgroundWatchersCacheTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/BackgroundWatchersCacheTest.java @@ -34,8 +34,8 @@ import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.contains; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileCacheTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileCacheTest.java index f7821f75..06a0345c 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileCacheTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileCacheTest.java @@ -37,8 +37,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.contains; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; /** * Tests for {@link DatafileCache} diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileClientTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileClientTest.java index fe7e80e6..c0ece2b5 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileClientTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileClientTest.java @@ -36,10 +36,10 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.contains; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java index 469edca9..931f005e 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java @@ -46,8 +46,8 @@ import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.mock; diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileReschedulerTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileReschedulerTest.java index d671e5e4..0c247b12 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileReschedulerTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileReschedulerTest.java @@ -37,7 +37,7 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileWorkerTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileWorkerTest.java index 02fce99a..036b00d3 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileWorkerTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileWorkerTest.java @@ -19,10 +19,9 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyObject; -import static org.mockito.Matchers.anyString; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/event-handler/build.gradle b/event-handler/build.gradle index 4e361dd1..1684d7c1 100644 --- a/event-handler/build.gradle +++ b/event-handler/build.gradle @@ -29,6 +29,11 @@ android { } testOptions { unitTests.returnDefaultValues = true + unitTests.all { + jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED' + jvmArgs '--add-opens=java.base/java.util.zip=ALL-UNNAMED' + jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED' + } } buildTypes { release { @@ -39,8 +44,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } buildToolsVersion build_tools_version } @@ -54,8 +59,7 @@ dependencies { testImplementation "junit:junit:$junit_ver" testImplementation "org.mockito:mockito-core:$mockito_ver" - testImplementation "org.powermock:powermock-module-junit4:$powermock_ver" - testImplementation "org.powermock:powermock-api-mockito2:$powermock_ver" + testImplementation "org.mockito:mockito-inline:$mockito_ver" testImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "androidx.work:work-testing:$work_runtime" @@ -67,9 +71,7 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" diff --git a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventClientTest.java b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventClientTest.java index a3ce2628..89b86fed 100644 --- a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventClientTest.java +++ b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventClientTest.java @@ -34,9 +34,9 @@ import java.net.HttpURLConnection; import java.net.URL; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyInt; -import static org.mockito.Matchers.contains; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.contains; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; diff --git a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventReschedulerTest.java b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventReschedulerTest.java index 18236dfa..725d2761 100644 --- a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventReschedulerTest.java +++ b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventReschedulerTest.java @@ -23,11 +23,11 @@ import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.slf4j.Logger; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.matches; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.matches; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; diff --git a/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventClientTest.java b/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventClientTest.java index 0f222273..7b42eaaf 100644 --- a/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventClientTest.java +++ b/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventClientTest.java @@ -24,7 +24,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.exceptions.base.MockitoException; -import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.junit.MockitoJUnitRunner; import org.slf4j.Logger; import java.io.IOException; @@ -35,8 +35,8 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventWorkerUnitTest.java b/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventWorkerUnitTest.java index 2baac029..3384e8d6 100644 --- a/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventWorkerUnitTest.java +++ b/event-handler/src/test/java/com/optimizely/ab/android/event_handler/EventWorkerUnitTest.java @@ -19,10 +19,14 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.anyString; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; + +import org.mockito.MockedStatic; + import android.content.Context; @@ -35,22 +39,17 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; -import org.powermock.api.mockito.PowerMockito; -import org.powermock.core.classloader.annotations.PowerMockIgnore; -import org.powermock.core.classloader.annotations.PrepareForTest; -import org.powermock.modules.junit4.PowerMockRunner; +import org.mockito.junit.MockitoJUnitRunner; import java.io.IOException; /** * Tests {@link EventWorker} */ -@RunWith(PowerMockRunner.class) -@PrepareForTest({ EventWorker.class, EventHandlerUtils.class, WorkerParameters.class }) -@PowerMockIgnore("jdk.internal.reflect.*") +@RunWith(MockitoJUnitRunner.class) public class EventWorkerUnitTest { - private WorkerParameters mockWorkParams = PowerMockito.mock(WorkerParameters.class); + private WorkerParameters mockWorkParams = mock(WorkerParameters.class); private EventWorker eventWorker = new EventWorker(mock(Context.class), mockWorkParams); private String host = "http://www.foo.com"; @@ -72,31 +71,37 @@ public void dataForCompressedEvent() { assertEquals(data.getString("bodyCompressed"), base64); assertNull(data.getString("body")); } - @Test public void compressEvent() throws IOException { String base64 = "abc123"; - PowerMockito.mockStatic(EventHandlerUtils.class); - when(EventHandlerUtils.compress(anyString())).thenReturn(base64); - Data data = EventWorker.compressEvent(host, smallBody); - assertEquals(data.getString("url"), host); - assertEquals(data.getString("bodyCompressed"), base64); - assertNull(data.getString("body")); + // Mocking the static method compress in EventHandlerUtils + try (MockedStatic<EventHandlerUtils> mockedStatic = mockStatic(EventHandlerUtils.class)) { + mockedStatic.when(() -> EventHandlerUtils.compress(anyString())).thenReturn(base64); + + Data data = EventWorker.compressEvent(host, smallBody); + + // Verify the results + assertEquals(data.getString("url"), host); + assertEquals(data.getString("bodyCompressed"), base64); + assertNull(data.getString("body")); + + // Optionally, verify that the method was called + mockedStatic.verify(() -> EventHandlerUtils.compress(anyString())); + } } + @Test public void compressEventWithCompressionFailure() throws IOException { - PowerMockito.mockStatic(EventHandlerUtils.class); - PowerMockito.doThrow(new IOException()).when(EventHandlerUtils.class); - EventHandlerUtils.compress(anyString()); // PowerMockito throws exception on this static method - - // return original body if compress fails - - Data data = EventWorker.compressEvent(host, smallBody); - assertEquals(data.getString("url"), host); - assertEquals(data.getString("body"), smallBody); - assertNull(data.getByteArray("bodyCompressed")); + try (MockedStatic<EventHandlerUtils> mockedStatic = mockStatic(EventHandlerUtils.class)) { + mockedStatic.when(() -> EventHandlerUtils.compress(anyString())).thenThrow(new IOException()); + // return original body if compress fails + Data data = EventWorker.compressEvent(host, smallBody); + assertEquals(data.getString("url"), host); + assertEquals(data.getString("body"), smallBody); + assertNull(data.getByteArray("bodyCompressed")); + } } @Test @@ -155,12 +160,17 @@ public void getEventBodyFromInputDataCompressed() { public void getEventBodyFromInputDataDecompressFailure() throws Exception { Data data = EventWorker.compressEvent(host, smallBody); - PowerMockito.mockStatic(EventHandlerUtils.class); - PowerMockito.doThrow(new IOException()).when(EventHandlerUtils.class); - EventHandlerUtils.decompress(any()); // PowerMockito throws exception on this static method + try (MockedStatic<EventHandlerUtils> mockedStatic = mockStatic(EventHandlerUtils.class)) { + mockedStatic.when(() -> EventHandlerUtils.compress(anyString())).thenThrow(new IOException()); - String str = eventWorker.getEventBodyFromInputData(data); - assertNull(str); + // return original body if compress fails + + EventHandlerUtils.decompress(any()); + + String str = eventWorker.getEventBodyFromInputData(data); + assertNull(str); + + } } @Test diff --git a/event-handler/src/test/resources/org.mockito.plugins.MockMaker b/event-handler/src/test/resources/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..1f0955d4 --- /dev/null +++ b/event-handler/src/test/resources/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/odp/build.gradle b/odp/build.gradle index 97c81043..fc7b85de 100644 --- a/odp/build.gradle +++ b/odp/build.gradle @@ -42,8 +42,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } buildToolsVersion build_tools_version @@ -57,6 +57,8 @@ dependencies { implementation "androidx.annotation:annotation:$annotations_ver" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.work:work-runtime:$work_runtime" + // Add SLF4J API for Logger interface + implementation "org.slf4j:slf4j-api:$slf4j_ver" testImplementation "junit:junit:$junit_ver" testImplementation "org.mockito:mockito-core:$mockito_ver" @@ -73,7 +75,6 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" + androidTestImplementation "org.slf4j:slf4j-api:$slf4j_ver" } diff --git a/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPEventClientTest.kt b/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPEventClientTest.kt index 21fa8589..c36e0655 100644 --- a/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPEventClientTest.kt +++ b/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPEventClientTest.kt @@ -24,10 +24,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Matchers.any -import org.mockito.Matchers.anyInt -import org.mockito.Matchers.contains -import org.mockito.Matchers.eq +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.contains +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify @@ -76,7 +75,7 @@ class ODPEventClientTest { eventClient.dispatch(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(2), eq(3)) val received = captor.value.execute() as Boolean assertFalse(received) @@ -91,7 +90,7 @@ class ODPEventClientTest { eventClient.dispatch(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(2), eq(3)) val received = captor.value.execute() as Boolean assertFalse(received) @@ -107,10 +106,10 @@ class ODPEventClientTest { apiEndpoint = "invalid-url" eventClient.dispatch(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(2), eq(3)) val received = captor.value.execute() as Boolean assertFalse(received) - verify(logger).error(contains("Error making request"), any()) + verify(logger).error(contains("Error making ODP event request"), any()) } } diff --git a/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPSegmentClientTest.kt b/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPSegmentClientTest.kt index b7e0f37e..511fa909 100644 --- a/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPSegmentClientTest.kt +++ b/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPSegmentClientTest.kt @@ -23,10 +23,9 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor -import org.mockito.Matchers.any -import org.mockito.Matchers.anyInt -import org.mockito.Matchers.contains -import org.mockito.Matchers.eq +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.contains +import org.mockito.ArgumentMatchers.eq import org.mockito.Mockito.`when` import org.mockito.Mockito.mock import org.mockito.Mockito.verify @@ -59,7 +58,7 @@ class ODPSegmentClientTest { segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), eq(2), eq(2)) + verify(client).execute(captor.capture(), eq(0), eq(0)) val received = captor.value.execute() assert(received == response) @@ -75,11 +74,11 @@ class ODPSegmentClientTest { segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(0), eq(0)) val received = captor.value.execute() assertNull(received) - verify(logger).error("Unexpected response from event endpoint, status: 400") + verify(logger).error("Unexpected response from ODP segment endpoint, status: 400") verify(urlConnection).disconnect() } @@ -89,11 +88,11 @@ class ODPSegmentClientTest { segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(0), eq(0)) val received = captor.value.execute() assertNull(received) - verify(logger).error("Unexpected response from event endpoint, status: 500") + verify(logger).error("Unexpected response from ODP segment endpoint, status: 500") verify(urlConnection).disconnect() } @@ -104,10 +103,10 @@ class ODPSegmentClientTest { apiEndpoint = "invalid-url" segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload) - verify(client).execute(captor.capture(), anyInt(), anyInt()) + verify(client).execute(captor.capture(), eq(0), eq(0)) val received = captor.value.execute() assertNull(received) - verify(logger).error(contains("Error making request"), any()) + verify(logger).error(contains("Error making ODP segment request"), any()) } } diff --git a/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt index cbbbfcfe..3151bf2c 100644 --- a/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt +++ b/odp/src/androidTest/java/com/optimizely/ab/android/odp/VuidManagerTest.kt @@ -22,6 +22,8 @@ import org.junit.After import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test @@ -40,7 +42,8 @@ class VuidManagerTest { // remove a singleton instance VuidManager.removeSharedForTesting() - vuidManager = VuidManager.getShared(context) + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) } @After @@ -51,6 +54,16 @@ class VuidManagerTest { editor.commit() } + fun saveInSharedPrefs(key: String, value: String) { + val sharedPreferences = context.getSharedPreferences("optly", Context.MODE_PRIVATE).edit() + sharedPreferences.putString(key, value) + sharedPreferences.apply() + } + + fun getFromSharedPrefs(key: String): String? { + return context.getSharedPreferences("optly", Context.MODE_PRIVATE).getString(key, null) + } + @Test fun makeVuid() { val vuid = vuidManager.makeVuid() @@ -90,16 +103,22 @@ class VuidManagerTest { @Test fun autoLoaded() { - val vuid1 = VuidManager.getShared(context).vuid + val vuidManager1 = VuidManager.getInstance() + vuidManager1.configure(true, context) + val vuid1 = vuidManager1.vuid assertTrue("vuid should be auto loaded when constructed", vuid1.startsWith("vuid_")) - val vuid2 = VuidManager.getShared(context).vuid + val vuidManager2 = VuidManager.getInstance() + vuidManager2.configure(true, context) + val vuid2 = vuidManager2.vuid assertEquals("the same vuid should be returned when getting a singleton", vuid1, vuid2) // remove shared instance, so will be re-instantiated VuidManager.removeSharedForTesting() - val vuid3 = VuidManager.getShared(context).vuid + val vuidManager3 = VuidManager.getInstance() + vuidManager3.configure(true, context) + val vuid3 = vuidManager3.vuid assertEquals("the saved vuid should be returned when instantiated again", vuid2, vuid3) // remove saved vuid @@ -107,8 +126,50 @@ class VuidManagerTest { // remove shared instance, so will be re-instantiated VuidManager.removeSharedForTesting() - val vuid4 = VuidManager.getShared(context).vuid + val vuidManager4 = VuidManager.getInstance() + vuidManager4.configure(true, context) + val vuid4 = vuidManager4.vuid assertNotEquals("a new vuid should be returned when storage cleared and re-instantiated", vuid3, vuid4) assertTrue(vuid4.startsWith("vuid_")) } + + @Test + fun configureWithVuidDisabled() { + cleanSharedPrefs() + saveInSharedPrefs("vuid", "vuid_test") + VuidManager.removeSharedForTesting() + + vuidManager = VuidManager.getInstance() + vuidManager.configure(false, context) + + assertNull(getFromSharedPrefs("vuid")) + assertEquals(vuidManager.vuid, "") + } + + @Test + fun configureWithVuidEnabledWhenVuidAlreadyExists() { + cleanSharedPrefs() + saveInSharedPrefs("vuid", "vuid_test") + VuidManager.removeSharedForTesting() + + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) + + assertEquals(vuidManager.vuid, "vuid_test") + } + + @Test + fun configureWithVuidEnabledWhenVuidDoesNotExist() { + cleanSharedPrefs() + VuidManager.removeSharedForTesting() + assertNull(getFromSharedPrefs("vuid")) + + vuidManager = VuidManager.getInstance() + vuidManager.configure(true, context) + + assertTrue(vuidManager.vuid.startsWith("vuid_")) + assertNotNull(getFromSharedPrefs("vuid")) + getFromSharedPrefs("vuid")?.let { assertTrue(it.startsWith("vuid_")) } + assertEquals(getFromSharedPrefs("vuid"), vuidManager.vuid) + } } diff --git a/odp/src/main/java/com/optimizely/ab/android/odp/ODPEventClient.kt b/odp/src/main/java/com/optimizely/ab/android/odp/ODPEventClient.kt index 1370bb76..a2d072d9 100644 --- a/odp/src/main/java/com/optimizely/ab/android/odp/ODPEventClient.kt +++ b/odp/src/main/java/com/optimizely/ab/android/odp/ODPEventClient.kt @@ -69,7 +69,7 @@ open class ODPEventClient(private val client: Client, private val logger: Logger return@Request false } } catch (e: Exception) { - logger.error("Error making request", e) + logger.error("Error making ODP event request", e) return@Request false } finally { if (urlConnection != null) { @@ -90,6 +90,10 @@ open class ODPEventClient(private val client: Client, private val logger: Logger var CONNECTION_TIMEOUT = 10 * 1000 var READ_TIMEOUT = 60 * 1000 + // OdpEventManager (java-sdk core) is supposed to handle retries on failures. + // android-sdk returns success immediately for sendOdpEvent() from OdpEventManager and schedules it via WorkManager. + // so retries on failure are supported here in OdpEventClient for android-sdk. + // the numerical base for the exponential backoff const val REQUEST_BACKOFF_TIMEOUT = 2 // power the number of retries diff --git a/odp/src/main/java/com/optimizely/ab/android/odp/ODPSegmentClient.kt b/odp/src/main/java/com/optimizely/ab/android/odp/ODPSegmentClient.kt index abab37cd..27287841 100644 --- a/odp/src/main/java/com/optimizely/ab/android/odp/ODPSegmentClient.kt +++ b/odp/src/main/java/com/optimizely/ab/android/odp/ODPSegmentClient.kt @@ -57,14 +57,15 @@ open class ODPSegmentClient(private val client: Client, private val logger: Logg val status = urlConnection.responseCode if (status in 200..399) { val json = client.readStream(urlConnection) - logger.debug("Successfully fetched segments: {}", json) + logger.debug("Successfully fetched ODP segments: {}", json) return@Request json } else { - logger.error("Unexpected response from event endpoint, status: $status") + var errMsg = "Unexpected response from ODP segment endpoint, status: $status" + logger.error(errMsg) return@Request null } } catch (e: Exception) { - logger.error("Error making request", e) + logger.error("Error making ODP segment request", e) return@Request null } finally { if (urlConnection != null) { @@ -92,9 +93,12 @@ open class ODPSegmentClient(private val client: Client, private val logger: Logg var CONNECTION_TIMEOUT = 10 * 1000 var READ_TIMEOUT = 60 * 1000 + // No retries on fetchQualifiedSegments() errors. + // We want to return failure immediately to callers. + // the numerical base for the exponential backoff - const val REQUEST_BACKOFF_TIMEOUT = 2 - // power the number of retries (2 = retry once) - const val REQUEST_RETRIES_POWER = 2 + const val REQUEST_BACKOFF_TIMEOUT = 0 + // power the number of retries + const val REQUEST_RETRIES_POWER = 0 } } diff --git a/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt index 5077141b..391b413c 100644 --- a/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt +++ b/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt @@ -19,20 +19,16 @@ import androidx.annotation.VisibleForTesting import com.optimizely.ab.android.shared.OptlyStorage import java.util.UUID -class VuidManager private constructor(context: Context) { +class VuidManager private constructor() { var vuid = "" private val keyForVuid = "vuid" // stored in the private "optly" storage - init { - this.vuid = load(context) - } - companion object { @Volatile private var INSTANCE: VuidManager? = null @Synchronized - fun getShared(context: Context): VuidManager = INSTANCE ?: VuidManager(context).also { INSTANCE = it } + fun getInstance(): VuidManager = INSTANCE ?: VuidManager().also { INSTANCE = it } fun isVuid(visitorId: String): Boolean { return visitorId.startsWith("vuid_", ignoreCase = true) @@ -44,6 +40,16 @@ class VuidManager private constructor(context: Context) { } } + @Synchronized + fun configure(enableVuid: Boolean, context: Context) { + if (!enableVuid) { + removeVuid(context) + this.vuid = "" + } else { + this.vuid = load(context) + } + } + @VisibleForTesting fun makeVuid(): String { val maxLength = 32 // required by ODP server @@ -57,7 +63,9 @@ class VuidManager private constructor(context: Context) { fun load(context: Context): String { val storage = OptlyStorage(context) val oldVuid = storage.getString(keyForVuid, null) - if (oldVuid != null) return oldVuid + if (oldVuid != null) { + return oldVuid + } val vuid = makeVuid() save(context, vuid) @@ -69,4 +77,9 @@ class VuidManager private constructor(context: Context) { val storage = OptlyStorage(context) storage.saveString(keyForVuid, vuid) } + + fun removeVuid(context: Context) { + val storage = OptlyStorage(context) + storage.remove(keyForVuid) + } } diff --git a/proguard-rules.txt b/proguard-rules.txt index d9a28f3a..f0197f30 100644 --- a/proguard-rules.txt +++ b/proguard-rules.txt @@ -24,6 +24,10 @@ -keepclassmembers class com.optimizely.ab.config.** { *; } +# Keep Payload classes that get sent to the ODP server +-keep class com.optimizely.ab.odp.ODPEvent { *; } +# ODP event uses this. R8 complains about it. +-dontwarn java.beans.Transient # Keep Payload classes that get sent to Optimizely's backend -keep class com.optimizely.ab.event.internal.payload.** { *; } @@ -81,5 +85,8 @@ # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken + +# Retain the name used by the Java SDK to determine whether Gson is usable as a config parser. +-keepnames class com.google.gson.Gson ##---------------End: proguard configuration for Gson ---------- diff --git a/shared/build.gradle b/shared/build.gradle index 06203d4e..2bab5c6d 100644 --- a/shared/build.gradle +++ b/shared/build.gradle @@ -41,8 +41,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } buildToolsVersion build_tools_version } @@ -51,6 +51,7 @@ dependencies { api ("com.optimizely.ab:core-api:$java_core_ver") { exclude group: 'com.google.code.findbugs' } + implementation "androidx.annotation:annotation:$annotations_ver" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "androidx.work:work-runtime:$work_runtime" @@ -74,9 +75,7 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" } diff --git a/shared/src/androidTest/java/com/optimizely/ab/android/shared/ClientTest.java b/shared/src/androidTest/java/com/optimizely/ab/android/shared/ClientTest.java index e1b3e29b..d35f644d 100644 --- a/shared/src/androidTest/java/com/optimizely/ab/android/shared/ClientTest.java +++ b/shared/src/androidTest/java/com/optimizely/ab/android/shared/ClientTest.java @@ -35,8 +35,8 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -144,4 +144,13 @@ public void testExpBackoffFailure() { assertTrue(timeouts.contains(8)); assertTrue(timeouts.contains(16)); } + + @Test + public void testExpBackoffFailure_noRetriesWhenBackoffSetToZero() { + Client.Request request = mock(Client.Request.class); + when(request.execute()).thenReturn(null); + assertNull(client.execute(request, 0, 0)); + verify(logger, never()).info(eq("Request failed, waiting {} seconds to try again"), any(Integer.class)); + } + } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/Client.java b/shared/src/main/java/com/optimizely/ab/android/shared/Client.java index e8d6c436..f2338eac 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/Client.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/Client.java @@ -162,6 +162,9 @@ public <T> T execute(Request<T> request, int timeout, int power) { } if (response == null || response == Boolean.FALSE) { + // retry is disabled when timeout set to 0 + if (timeout == 0) break; + try { logger.info("Request failed, waiting {} seconds to try again", timeout); Thread.sleep(TimeUnit.MILLISECONDS.convert(timeout, TimeUnit.SECONDS)); diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java index 260acbb7..83e22d09 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java @@ -91,4 +91,8 @@ private SharedPreferences.Editor getWritablePrefs() { private SharedPreferences getReadablePrefs() { return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); } + + public void remove(String key) { + getWritablePrefs().remove(key).apply(); + } } diff --git a/test-app/build.gradle b/test-app/build.gradle index e07aacc5..86653c09 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -17,14 +17,21 @@ android { unitTests.returnDefaultValues = true } buildTypes { + debug { + // enable proguard for debug mode (keep both of these to detect issues while testing) + minifyEnabled true + debuggable false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'test-app-proguard-rules.pro' + } release { minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'test-app-proguard-rules.pro' + signingConfig signingConfigs.debug } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } packagingOptions { resources { diff --git a/test-app/src/main/java/com/optimizely/ab/android/test_app/MyApplication.kt b/test-app/src/main/java/com/optimizely/ab/android/test_app/MyApplication.kt index ce16faaf..abfe9ce6 100644 --- a/test-app/src/main/java/com/optimizely/ab/android/test_app/MyApplication.kt +++ b/test-app/src/main/java/com/optimizely/ab/android/test_app/MyApplication.kt @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2016-2021, Optimizely, Inc. and contributors * + * Copyright 2016-2021, 2023 Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * diff --git a/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInJava.java b/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInJava.java index fdabf826..ccf09dfe 100644 --- a/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInJava.java +++ b/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInJava.java @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2020, 2022, Optimizely, Inc. and contributors * + * Copyright 2020, 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -90,9 +90,10 @@ static public void samplesAll(Context context) { samplesForDoc_NotificatonListener(context); samplesForDoc_OlderVersions(context); samplesForDoc_ForcedDecision(context); + samplesForDoc_ODP_async(context); + samplesForDoc_ODP_sync(context); } - static public void samplesForDecide(Context context) { // this default-options will be applied to all following decide calls. List<OptimizelyDecideOption> defaultDecideOptions = Arrays.asList(OptimizelyDecideOption.DISABLE_DECISION_EVENT); @@ -859,4 +860,31 @@ static public void samplesForDoc_ForcedDecision(Context context) { success = user.removeAllForcedDecisions(); } -} \ No newline at end of file + static public void samplesForDoc_ODP_async(Context context) { + OptimizelyManager optimizelyManager = OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context); + optimizelyManager.initialize(context, null, (OptimizelyClient client) -> { + OptimizelyUserContext userContext = client.createUserContext("user_123"); + userContext.fetchQualifiedSegments((status) -> { + Log.d("Optimizely", "[ODP] segments = " + userContext.getQualifiedSegments()); + OptimizelyDecision optDecision = userContext.decide("odp-flag-1"); + Log.d("Optimizely", "[ODP] decision = " + optDecision.toString()); + }); + }); + } + + static public void samplesForDoc_ODP_sync(Context context) { + OptimizelyManager optimizelyManager = OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context); + + boolean returnInMainThread = false; + + optimizelyManager.initialize(context, null, returnInMainThread, (OptimizelyClient client) -> { + OptimizelyUserContext userContext = client.createUserContext("user_123"); + userContext.fetchQualifiedSegments(); + + Log.d("Optimizely", "[ODP] segments = " + userContext.getQualifiedSegments()); + OptimizelyDecision optDecision = userContext.decide("odp-flag-1"); + Log.d("Optimizely", "[ODP] decision = " + optDecision.toString()); + }); + } + +} diff --git a/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInKotlin.kt b/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInKotlin.kt index 1107d5de..d2112caf 100644 --- a/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInKotlin.kt +++ b/test-app/src/main/java/com/optimizely/ab/android/test_app/Samples/APISamplesInKotlin.kt @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2022, Optimizely, Inc. and contributors * + * Copyright 2022-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -18,8 +18,6 @@ package com.optimizely.ab.android.test_app import android.content.Context import android.content.IntentFilter import android.net.wifi.WifiManager -import android.os.Parcel -import android.os.Parcelable import android.util.Log import com.optimizely.ab.OptimizelyDecisionContext import com.optimizely.ab.OptimizelyForcedDecision @@ -29,7 +27,6 @@ import com.optimizely.ab.android.event_handler.EventRescheduler import com.optimizely.ab.android.sdk.OptimizelyClient import com.optimizely.ab.android.sdk.OptimizelyManager import com.optimizely.ab.bucketing.UserProfileService -import com.optimizely.ab.config.Variation import com.optimizely.ab.config.parser.JsonParseException import com.optimizely.ab.error.ErrorHandler import com.optimizely.ab.error.RaiseExceptionErrorHandler @@ -40,12 +37,8 @@ import com.optimizely.ab.notification.DecisionNotification import com.optimizely.ab.notification.NotificationHandler import com.optimizely.ab.notification.TrackNotification import com.optimizely.ab.notification.UpdateConfigNotification -import com.optimizely.ab.optimizelyconfig.OptimizelyConfig import com.optimizely.ab.optimizelydecision.OptimizelyDecideOption import com.optimizely.ab.optimizelydecision.OptimizelyDecision -import com.optimizely.ab.optimizelyjson.OptimizelyJSON -import org.slf4j.LoggerFactory -import java.lang.Exception import java.util.* import java.util.concurrent.TimeUnit @@ -76,6 +69,8 @@ object APISamplesInKotlin { samplesForDoc_NotificatonListener(context) samplesForDoc_OlderVersions(context) samplesForDoc_ForcedDecision(context) + samplesForDoc_ODP_async(context) + samplesForDoc_ODP_sync(context) } fun samplesForDecide(context: Context) { @@ -828,6 +823,35 @@ object APISamplesInKotlin { success = user.removeAllForcedDecisions() } + fun samplesForDoc_ODP_async(context: Context?) { + val optimizelyManager = + OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context) + optimizelyManager.initialize(context!!, null) { client: OptimizelyClient -> + val userContext = client.createUserContext("user_123") + userContext!!.fetchQualifiedSegments { status: Boolean? -> + Log.d("Optimizely", "[ODP] segments = " + userContext.qualifiedSegments) + val optDecision = userContext.decide("odp-flag-1") + Log.d("Optimizely", "[ODP] decision = $optDecision") + } + } + } + + fun samplesForDoc_ODP_sync(context: Context?) { + val optimizelyManager = + OptimizelyManager.builder().withSDKKey("VivZyCGPHY369D4z8T9yG").build(context) + + val returnInMainThread = false; + + optimizelyManager.initialize(context!!, null, returnInMainThread) { client: OptimizelyClient -> + val userContext = client.createUserContext("user_123") + userContext!!.fetchQualifiedSegments() + + Log.d("Optimizely", "[ODP] segments = " + userContext.qualifiedSegments) + val optDecision = userContext.decide("odp-flag-1") + Log.d("Optimizely", "[ODP] decision = $optDecision") + } + } + } diff --git a/test-app/src/main/java/com/optimizely/ab/android/test_app/SplashScreenActivity.kt b/test-app/src/main/java/com/optimizely/ab/android/test_app/SplashScreenActivity.kt index 3ee7f38b..36ea2e1a 100644 --- a/test-app/src/main/java/com/optimizely/ab/android/test_app/SplashScreenActivity.kt +++ b/test-app/src/main/java/com/optimizely/ab/android/test_app/SplashScreenActivity.kt @@ -25,6 +25,7 @@ import com.optimizely.ab.android.event_handler.EventRescheduler import com.optimizely.ab.android.sdk.OptimizelyClient import com.optimizely.ab.android.sdk.OptimizelyManager import com.optimizely.ab.android.shared.CountingIdlingResourceManager +import com.optimizely.ab.android.test_app.Samples.APISamplesInJava import com.optimizely.ab.notification.DecisionNotification import com.optimizely.ab.notification.TrackNotification import com.optimizely.ab.notification.UpdateConfigNotification @@ -131,4 +132,4 @@ class SplashScreenActivity : AppCompatActivity() { // The Idling Resource which will be null in production. private val countingIdlingResourceManager: CountingIdlingResourceManager? = null } -} \ No newline at end of file +} diff --git a/test-app/proguard-rules.pro b/test-app/test-app-proguard-rules.pro similarity index 100% rename from test-app/proguard-rules.pro rename to test-app/test-app-proguard-rules.pro diff --git a/user-profile/build.gradle b/user-profile/build.gradle index cfc7fee3..238263fe 100644 --- a/user-profile/build.gradle +++ b/user-profile/build.gradle @@ -39,8 +39,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } @@ -65,9 +65,7 @@ dependencies { androidTestImplementation "androidx.test:core-ktx:$androidx_test_core" androidTestImplementation "org.mockito:mockito-core:$mockito_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver" - androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver" + androidTestImplementation "org.mockito:mockito-android:$mockito_ver" androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver" androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion" } diff --git a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DefaultUserProfileServiceTest.java b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DefaultUserProfileServiceTest.java index 0390922d..87c84c58 100644 --- a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DefaultUserProfileServiceTest.java +++ b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DefaultUserProfileServiceTest.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -39,6 +40,8 @@ import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; /** * Tests for {@link DefaultUserProfileService} @@ -51,7 +54,6 @@ public class DefaultUserProfileServiceTest { private UserProfileCache.DiskCache diskCache; private ExecutorService executor; private Logger logger; - private UserProfileCache.LegacyDiskCache legacyDiskCache; private Map<String, Map<String, Object>> memoryCache; private String projectId; private UserProfileCache userProfileCache; @@ -66,11 +68,10 @@ public void setup() { logger = mock(Logger.class); cache = new Cache(InstrumentationRegistry.getInstrumentation().getTargetContext(), logger); executor =Executors.newSingleThreadExecutor(); - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); memoryCache = new ConcurrentHashMap<>(); projectId = "123"; diskCache = new UserProfileCache.DiskCache(cache, executor, logger, projectId); - userProfileCache = new UserProfileCache(diskCache, logger, memoryCache, legacyDiskCache); + userProfileCache = new UserProfileCache(diskCache, logger, memoryCache); userProfileService = new DefaultUserProfileService(userProfileCache, logger); // Test data. @@ -103,6 +104,20 @@ public void teardown() { cache.delete(diskCache.getFileName()); } + @Test + public void startInBackground() throws InterruptedException { + DefaultUserProfileService ups = spy(userProfileService); + + CountDownLatch latch = new CountDownLatch(1); + ups.startInBackground((u) -> { + latch.countDown(); + }); + + latch.await(3, TimeUnit.SECONDS); + + verify(ups).start(); + } + @Test public void saveAndStartAndLookup() { userProfileService.save(userProfileMap1); diff --git a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DiskCacheTest.java b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DiskCacheTest.java index 684bb52d..b7e3c31b 100644 --- a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DiskCacheTest.java +++ b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/DiskCacheTest.java @@ -38,8 +38,8 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; diff --git a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/LegacyDiskCacheTest.java b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/LegacyDiskCacheTest.java deleted file mode 100644 index 1cfa7001..00000000 --- a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/LegacyDiskCacheTest.java +++ /dev/null @@ -1,132 +0,0 @@ -/**************************************************************************** - * Copyright 2021, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -package com.optimizely.ab.android.user_profile; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.platform.app.InstrumentationRegistry; - -import com.optimizely.ab.android.shared.Cache; - -import org.json.JSONException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.slf4j.Logger; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; - -import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.fail; -import static org.mockito.Matchers.any; -import static org.mockito.Matchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -/** - * Tests for {@link UserProfileCache.LegacyDiskCache} - */ -@RunWith(AndroidJUnit4.class) -public class LegacyDiskCacheTest { - - // Runs tasks serially on the calling thread - private ExecutorService executor = Executors.newSingleThreadExecutor(); - private Cache cache; - private Logger logger; - private UserProfileCache.LegacyDiskCache legacyDiskCache; - private String projectId; - - @Before - public void setup() { - logger = mock(Logger.class); - cache = new Cache(InstrumentationRegistry.getInstrumentation().getTargetContext(), logger); - projectId = "123"; - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); - } - - @After - public void teardown() { - cache.delete(legacyDiskCache.getFileName()); - } - - @Test - public void testGetFileName() { - assertEquals("optly-user-profile-123.json", legacyDiskCache.getFileName()); - } - - @Test - public void testLoadWhenNoFile() throws JSONException { - assertNull(legacyDiskCache.load()); - verify(logger).warn("Unable to load file {}.", legacyDiskCache.getFileName()); - verify(logger).info("Legacy user profile cache not found."); - } - - @Test - public void testLoadMalformedCache() throws JSONException { - cache = mock(Cache.class); - when(cache.load(legacyDiskCache.getFileName())).thenReturn("{?}"); - when(cache.delete(legacyDiskCache.getFileName())).thenReturn(true); - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); - - assertNull(legacyDiskCache.load()); - try { - executor.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - fail("Timed out"); - } - - verify(logger).info("Deleted legacy user profile from disk."); - verify(logger).warn(eq("Unable to parse legacy user profiles. Will delete legacy user profile cache file."), - any(Exception.class)); - } - - @Test - public void testDelete() throws JSONException { - cache = mock(Cache.class); - when(cache.delete(legacyDiskCache.getFileName())).thenReturn(true); - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); - - legacyDiskCache.delete(); - try { - executor.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - fail("Timed out"); - } - - verify(logger).info("Deleted legacy user profile from disk."); - } - - @Test - public void testDeleteFailed() throws JSONException { - cache = mock(Cache.class); - when(cache.delete(legacyDiskCache.getFileName())).thenReturn(false); - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); - - legacyDiskCache.delete(); - try { - executor.awaitTermination(5, TimeUnit.SECONDS); - } catch (InterruptedException e) { - fail("Timed out"); - } - - verify(logger).warn("Unable to delete legacy user profile from disk."); - } -} diff --git a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/UserProfileCacheTest.java b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/UserProfileCacheTest.java index acc13b4a..ee4fde74 100644 --- a/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/UserProfileCacheTest.java +++ b/user-profile/src/androidTest/java/com/optimizely/ab/android/user_profile/UserProfileCacheTest.java @@ -53,7 +53,6 @@ public class UserProfileCacheTest { private Logger logger; private Cache cache; private UserProfileCache.DiskCache diskCache; - private UserProfileCache.LegacyDiskCache legacyDiskCache; private Map<String, Map<String, Object>> memoryCache; private String projectId; private UserProfileCache userProfileCache; @@ -69,9 +68,8 @@ public void setup() throws JSONException { projectId = "1"; cache = new Cache(InstrumentationRegistry.getInstrumentation().getTargetContext(), logger); diskCache = new UserProfileCache.DiskCache(cache, executor, logger, projectId); - legacyDiskCache = new UserProfileCache.LegacyDiskCache(cache, executor, logger, projectId); memoryCache = new ConcurrentHashMap<>(); - userProfileCache = new UserProfileCache(diskCache, logger, memoryCache, legacyDiskCache); + userProfileCache = new UserProfileCache(diskCache, logger, memoryCache); // Test data. userId1 = "user_1"; diff --git a/user-profile/src/main/java/com/optimizely/ab/android/user_profile/DefaultUserProfileService.java b/user-profile/src/main/java/com/optimizely/ab/android/user_profile/DefaultUserProfileService.java index 11cd64b5..d5f3aa33 100644 --- a/user-profile/src/main/java/com/optimizely/ab/android/user_profile/DefaultUserProfileService.java +++ b/user-profile/src/main/java/com/optimizely/ab/android/user_profile/DefaultUserProfileService.java @@ -20,6 +20,9 @@ import android.content.Context; import android.os.AsyncTask; import android.annotation.TargetApi; +import android.os.Handler; +import android.os.Looper; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -32,6 +35,7 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** @@ -44,8 +48,10 @@ */ public class DefaultUserProfileService implements UserProfileService { - @NonNull private final UserProfileCache userProfileCache; - @NonNull private final Logger logger; + @NonNull + private final UserProfileCache userProfileCache; + @NonNull + private final Logger logger; DefaultUserProfileService(@NonNull UserProfileCache userProfileCache, @NonNull Logger logger) { this.userProfileCache = userProfileCache; @@ -62,17 +68,20 @@ public class DefaultUserProfileService implements UserProfileService { */ public static UserProfileService newInstance(@NonNull String projectId, @NonNull Context context) { UserProfileCache userProfileCache = new UserProfileCache( - new UserProfileCache.DiskCache(new Cache(context, LoggerFactory.getLogger(Cache.class)), - Executors.newSingleThreadExecutor(), LoggerFactory.getLogger(UserProfileCache.DiskCache.class), - projectId), - LoggerFactory.getLogger(UserProfileCache.class), - new ConcurrentHashMap<String, Map<String, Object>>(), - new UserProfileCache.LegacyDiskCache(new Cache(context, LoggerFactory.getLogger(Cache.class)), - Executors.newSingleThreadExecutor(), - LoggerFactory.getLogger(UserProfileCache.LegacyDiskCache.class), projectId)); + new UserProfileCache.DiskCache( + new Cache( + context, + LoggerFactory.getLogger(Cache.class) + ), + Executors.newSingleThreadExecutor(), + LoggerFactory.getLogger(UserProfileCache.DiskCache.class), + projectId + ), + LoggerFactory.getLogger(UserProfileCache.class), + new ConcurrentHashMap<String, Map<String, Object>>()); return new DefaultUserProfileService(userProfileCache, - LoggerFactory.getLogger(DefaultUserProfileService.class)); + LoggerFactory.getLogger(DefaultUserProfileService.class)); } public interface StartCallback { @@ -80,30 +89,35 @@ public interface StartCallback { } public void startInBackground(final StartCallback callback) { - final DefaultUserProfileService userProfileService = this; + startInBackground(callback, true); + } - AsyncTask<Void, Void, UserProfileService> initUserProfileTask = new AsyncTask<Void, Void, UserProfileService>() { - @Override - protected UserProfileService doInBackground(Void[] params) { - userProfileService.start(); - return userProfileService; - } + public void startInBackground(final StartCallback callback, boolean returnOnMainThread) { + final DefaultUserProfileService userProfileService = this; + Handler mainHandler = new Handler(Looper.getMainLooper()); + + Runnable initUserProfileTask = new Runnable() { @Override - protected void onPostExecute(UserProfileService userProfileService) { + public void run() { + userProfileService.start(); + if (callback != null) { - callback.onStartComplete(userProfileService); + if (returnOnMainThread) { + mainHandler.post(new Runnable() { + @Override + public void run() { + callback.onStartComplete(userProfileService); + } + }); + } else { + callback.onStartComplete(userProfileService); + } } } }; - try { - initUserProfileTask.executeOnExecutor(Executors.newSingleThreadExecutor()); - } - catch (Exception e) { - logger.error("Error loading user profile service from AndroidUserProfileServiceDefault"); - callback.onStartComplete(null); - } - + ExecutorService executor = Executors.newSingleThreadExecutor(); + executor.submit(initUserProfileTask); } /** @@ -143,15 +157,15 @@ public void remove(String userId) { public void removeInvalidExperiments(Set<String> validExperiments) { try { userProfileCache.removeInvalidExperiments(validExperiments); - } - catch (Exception e) { + } catch (Exception e) { logger.error("Error calling userProfileCache to remove invalid experiments", e); } } + /** * Remove a decision from a user profile. * - * @param userId the user ID of the decision to remove + * @param userId the user ID of the decision to remove * @param experimentId the experiment ID of the decision to remove */ public void remove(String userId, String experimentId) { diff --git a/user-profile/src/main/java/com/optimizely/ab/android/user_profile/UserProfileCache.java b/user-profile/src/main/java/com/optimizely/ab/android/user_profile/UserProfileCache.java index 8e90f271..671fd98c 100644 --- a/user-profile/src/main/java/com/optimizely/ab/android/user_profile/UserProfileCache.java +++ b/user-profile/src/main/java/com/optimizely/ab/android/user_profile/UserProfileCache.java @@ -47,15 +47,13 @@ class UserProfileCache { @NonNull @VisibleForTesting protected final DiskCache diskCache; @NonNull private final Logger logger; @NonNull private final Map<String, Map<String, Object>> memoryCache; - @NonNull private final LegacyDiskCache legacyDiskCache; - UserProfileCache(@NonNull DiskCache diskCache, @NonNull Logger logger, - @NonNull Map<String, Map<String, Object>> memoryCache, - @NonNull LegacyDiskCache legacyDiskCache) { + UserProfileCache(@NonNull DiskCache diskCache, + @NonNull Logger logger, + @NonNull Map<String, Map<String, Object>> memoryCache) { this.logger = logger; this.diskCache = diskCache; this.memoryCache = memoryCache; - this.legacyDiskCache = legacyDiskCache; } /** @@ -85,49 +83,6 @@ Map<String, Object> lookup(String userId) { return memoryCache.get(userId); } - /** - * Migrate legacy user profiles if found. - * <p> - * Note: this will overwrite a newer `UserProfile` cache in the unlikely event that a legacy cache and new cache - * both exist on disk. - */ - @VisibleForTesting - void migrateLegacyUserProfiles() { - JSONObject legacyUserProfilesJson = legacyDiskCache.load(); - - if (legacyUserProfilesJson == null) { - logger.info("No legacy user profiles to migrate."); - return; - } - - try { - Iterator<String> userIdIterator = legacyUserProfilesJson.keys(); - while (userIdIterator.hasNext()) { - String userId = userIdIterator.next(); - JSONObject legacyUserProfileJson = legacyUserProfilesJson.getJSONObject(userId); - - Map<String, Map<String, String>> experimentBucketMap = new ConcurrentHashMap<>(); - Iterator<String> experimentIdIterator = legacyUserProfileJson.keys(); - while (experimentIdIterator.hasNext()) { - String experimentId = experimentIdIterator.next(); - String variationId = legacyUserProfileJson.getString(experimentId); - Map<String, String> decisionMap = new ConcurrentHashMap<>(); - decisionMap.put(variationIdKey, variationId); - experimentBucketMap.put(experimentId, decisionMap); - } - - Map<String, Object> userProfileMap = new ConcurrentHashMap<>(); - userProfileMap.put(userIdKey, userId); - userProfileMap.put(experimentBucketMapKey, experimentBucketMap); - save(userProfileMap); - } - } catch (JSONException e) { - logger.warn("Unable to deserialize legacy user profiles. Will delete legacy user profile cache file.", e); - } finally { - legacyDiskCache.delete(); - } - } - /** * Remove a user profile. * @@ -218,9 +173,6 @@ void save(Map<String, Object> userProfileMap) { * Load the cache from disk to memory. */ void start() { - // Migrate legacy user profiles if found. - migrateLegacyUserProfiles(); - try { JSONObject userProfilesJson = diskCache.load(); Map<String, Map<String, Object>> userProfilesMap = UserProfileCacheUtils.convertJSONObjectToMap @@ -295,71 +247,4 @@ protected Boolean doInBackground(Void[] params) { task.executeOnExecutor(executor); } } - - /** - * Stores a map of userIds to a map of expIds to variationIds in a file. - * - * @deprecated This class is only used to migrate legacy user profiles to the new {@link UserProfileCache}. - */ - static class LegacyDiskCache { - - private static final String FILE_NAME = "optly-user-profile-%s.json"; - @NonNull private final Cache cache; - @NonNull private final Executor executor; - @NonNull private final Logger logger; - @NonNull private final String projectId; - - LegacyDiskCache(@NonNull Cache cache, @NonNull Executor executor, @NonNull Logger logger, - @NonNull String projectId) { - this.cache = cache; - this.executor = executor; - this.logger = logger; - this.projectId = projectId; - } - - @VisibleForTesting - String getFileName() { - return String.format(FILE_NAME, projectId); - } - - /** - * Load legacy user profiles from disk if found. - */ - @Nullable - JSONObject load() { - String cacheString = cache.load(getFileName()); - - if (cacheString == null) { - logger.info("Legacy user profile cache not found."); - return null; - } - - try { - return new JSONObject(cacheString); - } catch (JSONException e) { - logger.warn("Unable to parse legacy user profiles. Will delete legacy user profile cache file.", e); - delete(); - return null; - } - } - - /** - * Delete the legacy user profile cache from disk in a background thread. - */ - void delete() { - AsyncTask<Void, Void, Boolean> task = new AsyncTask<Void, Void, Boolean>() { - @Override - protected Boolean doInBackground(Void[] params) { - Boolean deleted = cache.delete(getFileName()); - if (deleted) { - logger.info("Deleted legacy user profile from disk."); - } else { - logger.warn("Unable to delete legacy user profile from disk."); - } - return deleted; - } - }; - task.executeOnExecutor(executor); - } - } }