├── .editorconfig
├── .github
├── ISSUE_TEMPLATE
│ ├── BUG-REPORT.yml
│ ├── ENHANCEMENT.yml
│ ├── FEATURE-REQUEST.md
│ └── config.yml
├── pull_request_template.md
└── workflows
│ ├── android.yml
│ ├── build.yml
│ ├── source_clear_cron.yml
│ └── ticket_reference_check.yml
├── .gitignore
├── .idea
└── copyright
│ ├── FullStack_Copyright.xml
│ └── profiles_settings.xml
├── CHANGELOG.md
├── CODEOWNERS
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── android-sdk
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── sdk
│ │ ├── ODPIntegrationTest.java
│ │ ├── ODPIntegrationUpdateConfigTest.java
│ │ ├── OptimizelyClientEngineTest.java
│ │ ├── OptimizelyClientTest.java
│ │ ├── OptimizelyDefaultAttributesTest.java
│ │ ├── OptimizelyManagerBuilderTest.java
│ │ ├── OptimizelyManagerEventHandlerTest.java
│ │ └── OptimizelyManagerTest.java
│ ├── debug
│ └── res
│ │ └── raw
│ │ ├── datafile
│ │ ├── datafile_api
│ │ ├── emptydatafile
│ │ ├── validprojectconfigv3
│ │ └── validprojectconfigv4
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── optimizely
│ │ │ └── ab
│ │ │ └── android
│ │ │ └── sdk
│ │ │ ├── OptimizelyClient.java
│ │ │ ├── OptimizelyClientEngine.java
│ │ │ ├── OptimizelyDefaultAttributes.java
│ │ │ ├── OptimizelyLiteLogger.java
│ │ │ ├── OptimizelyManager.java
│ │ │ └── OptimizelyStartListener.java
│ └── resources
│ │ └── android-logger.properties
│ └── test
│ └── java
│ └── com
│ └── optimizely
│ └── ab
│ └── android
│ └── sdk
│ ├── OptimizelyClientTest.java
│ ├── OptimizelyManagerBuilderTest.java
│ ├── OptimizelyManagerIntervalTest.java
│ └── OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java
├── build.gradle
├── datafile-handler
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── datafile_handler
│ │ ├── BackgroundWatchersCacheTest.java
│ │ ├── DatafileCacheTest.java
│ │ ├── DatafileClientTest.java
│ │ ├── DatafileLoaderTest.java
│ │ ├── DatafileReschedulerTest.java
│ │ ├── DatafileWorkerTest.java
│ │ └── DefaultDatafileHandlerTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── optimizely
│ │ │ └── ab
│ │ │ └── android
│ │ │ └── datafile_handler
│ │ │ ├── BackgroundWatchersCache.java
│ │ │ ├── DatafileCache.java
│ │ │ ├── DatafileClient.java
│ │ │ ├── DatafileHandler.java
│ │ │ ├── DatafileLoadedListener.java
│ │ │ ├── DatafileLoader.java
│ │ │ ├── DatafileRescheduler.java
│ │ │ ├── DatafileWorker.java
│ │ │ └── DefaultDatafileHandler.java
│ └── res
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── optimizely
│ └── ab
│ └── android
│ └── datafile_handler
│ └── DefaultDatafileHandlerUnitTest.java
├── docs
└── readme.md
├── event-handler
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── event_handler
│ │ ├── DefaultEventHandlerTest.java
│ │ ├── EventClientTest.java
│ │ ├── EventDAOTest.java
│ │ ├── EventReschedulerTest.java
│ │ ├── EventSQLiteOpenHelperTest.java
│ │ └── EventWorkerTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── event_handler
│ │ ├── DefaultEventHandler.java
│ │ ├── Event.java
│ │ ├── EventClient.java
│ │ ├── EventDAO.java
│ │ ├── EventDispatcher.java
│ │ ├── EventRescheduler.java
│ │ ├── EventSQLiteOpenHelper.java
│ │ ├── EventTable.java
│ │ └── EventWorker.java
│ └── test
│ ├── java
│ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── event_handler
│ │ ├── EventClientTest.java
│ │ └── EventWorkerUnitTest.java
│ └── resources
│ └── org.mockito.plugins.MockMaker
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── odp
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── odp
│ │ ├── DefaultODPApiManagerTest.kt
│ │ ├── ODPEventClientTest.kt
│ │ ├── ODPEventWorkerTest.kt
│ │ ├── ODPSegmentClientTest.kt
│ │ └── VuidManagerTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── optimizely
│ └── ab
│ └── android
│ └── odp
│ ├── DefaultODPApiManager.kt
│ ├── ODPEventClient.kt
│ ├── ODPEventWorker.kt
│ ├── ODPSegmentClient.kt
│ └── VuidManager.kt
├── proguard-rules.txt
├── scripts
└── android-wait-for-emulator.sh
├── settings.gradle
├── shared
├── .gitignore
├── build.gradle
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── shared
│ │ ├── CacheTest.java
│ │ ├── ClientTest.java
│ │ ├── OptlyStorageTest.java
│ │ ├── TestWorker.java
│ │ └── WorkerSchedulerTest.java
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── optimizely
│ │ └── ab
│ │ └── android
│ │ └── shared
│ │ ├── Cache.java
│ │ ├── CachedCounter.java
│ │ ├── Client.java
│ │ ├── CountingIdlingResourceInterface.java
│ │ ├── CountingIdlingResourceManager.java
│ │ ├── DatafileConfig.java
│ │ ├── EventHandlerUtils.java
│ │ ├── OptlyStorage.java
│ │ ├── TLSSocketFactory.java
│ │ └── WorkerScheduler.java
│ └── test
│ └── java
│ └── com
│ └── optimizely
│ └── ab
│ └── android
│ └── shared
│ └── EventHandlerUtilsTest.java
├── test-app
├── .gitignore
├── README.md
├── build.gradle
├── images
│ └── demo-app-flow.png
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ └── com
│ │ │ └── optimizely
│ │ │ └── ab
│ │ │ └── android
│ │ │ └── test_app
│ │ │ ├── ActivationErrorActivity.kt
│ │ │ ├── BaseActivity.kt
│ │ │ ├── ConversionFragment.kt
│ │ │ ├── CouponActivity.kt
│ │ │ ├── EventConfirmationActivity.kt
│ │ │ ├── MyApplication.kt
│ │ │ ├── NotificationService.kt
│ │ │ ├── Samples
│ │ │ ├── APISamplesInJava.java
│ │ │ └── APISamplesInKotlin.kt
│ │ │ ├── SplashScreenActivity.kt
│ │ │ ├── VariationAActivity.kt
│ │ │ └── VariationBActivity.kt
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── ic_background_confirmation.xml
│ │ │ ├── ic_background_error.xml
│ │ │ ├── ic_background_varia.xml
│ │ │ ├── ic_background_varib_marina.xml
│ │ │ └── ic_optimizely_logo.xml
│ │ ├── layout
│ │ │ ├── activity_activation_error.xml
│ │ │ ├── activity_event_confirmation.xml
│ │ │ ├── activity_splash_screen.xml
│ │ │ ├── activity_variation_a.xml
│ │ │ ├── activity_variation_b.xml
│ │ │ ├── coupon.xml
│ │ │ └── fragment_conversion.xml
│ │ ├── mipmap-hdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-mdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ │ └── ic_launcher.png
│ │ ├── raw
│ │ │ └── datafile.json
│ │ ├── values-w820dp
│ │ │ └── dimens.xml
│ │ └── values
│ │ │ ├── colors.xml
│ │ │ ├── dimens.xml
│ │ │ ├── strings.xml
│ │ │ └── styles.xml
│ │ └── resources
│ │ └── android-logger.properties
└── test-app-proguard-rules.pro
└── user-profile
├── .gitignore
├── build.gradle
└── src
├── androidTest
└── java
│ └── com
│ └── optimizely
│ └── ab
│ └── android
│ └── user_profile
│ ├── DefaultUserProfileServiceTest.java
│ ├── DiskCacheTest.java
│ ├── UserProfileCacheTest.java
│ └── UserProfileCacheUtilsTest.java
└── main
├── AndroidManifest.xml
└── java
└── com
└── optimizely
└── ab
└── android
└── user_profile
├── DefaultUserProfileService.java
├── UserProfileCache.java
└── UserProfileCacheUtils.java
/.editorconfig:
--------------------------------------------------------------------------------
1 | # indicate this is the root of the project
2 | root = true
3 |
4 | [*.{kt,java,xml,gradle,md}]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 4
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 | end_of_line = lf
11 |
12 | [{*.gradle.kts,*.kt,*.kts,*.main.kts}]
13 | disabled_rules = import-ordering
14 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/BUG-REPORT.yml:
--------------------------------------------------------------------------------
1 | name: 🐞 Bug
2 | description: File a bug/issue
3 | title: "[BUG]
"
4 | labels: ["bug", "needs-triage"]
5 | body:
6 | - type: checkboxes
7 | attributes:
8 | label: Is there an existing issue for this?
9 | description: Please search to see if an issue already exists for the bug you encountered.
10 | options:
11 | - label: I have searched the existing issues
12 | required: true
13 | - type: textarea
14 | attributes:
15 | label: SDK Version
16 | description: Version of the SDK in use?
17 | validations:
18 | required: true
19 | - type: textarea
20 | attributes:
21 | label: Current Behavior
22 | description: A concise description of what you're experiencing.
23 | validations:
24 | required: true
25 | - type: textarea
26 | attributes:
27 | label: Expected Behavior
28 | description: A concise description of what you expected to happen.
29 | validations:
30 | required: true
31 | - type: textarea
32 | attributes:
33 | label: Steps To Reproduce
34 | description: Steps to reproduce the behavior.
35 | placeholder: |
36 | 1. In this environment...
37 | 2. With this config...
38 | 3. Run '...'
39 | 4. See error...
40 | validations:
41 | required: true
42 | - type: textarea
43 | attributes:
44 | label: Link
45 | description: Link to code demonstrating the problem.
46 | validations:
47 | required: false
48 | - type: textarea
49 | attributes:
50 | label: Logs
51 | description: Logs/stack traces related to the problem (⚠️do not include sensitive information).
52 | validations:
53 | required: false
54 | - type: dropdown
55 | attributes:
56 | label: Severity
57 | description: What is the severity of the problem?
58 | multiple: true
59 | options:
60 | - Blocking development
61 | - Affecting users
62 | - Minor issue
63 | validations:
64 | required: false
65 | - type: textarea
66 | attributes:
67 | label: Workaround/Solution
68 | description: Do you have any workaround or solution in mind for the problem?
69 | validations:
70 | required: false
71 | - type: textarea
72 | attributes:
73 | label: "Recent Change"
74 | description: Has this issue started happening after an update or experiment change?
75 | validations:
76 | required: false
77 | - type: textarea
78 | attributes:
79 | label: Conflicts
80 | description: Are there other libraries/dependencies potentially in conflict?
81 | validations:
82 | required: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml:
--------------------------------------------------------------------------------
1 | name: ✨Enhancement
2 | 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.
3 | title: "[ENHANCEMENT] "
4 | labels: ["enhancement"]
5 | body:
6 | - type: textarea
7 | id: description
8 | attributes:
9 | label: "Description"
10 | description: Briefly describe the enhancement in a few sentences.
11 | placeholder: Short description...
12 | validations:
13 | required: true
14 | - type: textarea
15 | id: benefits
16 | attributes:
17 | label: "Benefits"
18 | description: How would the enhancement benefit to your product or usage?
19 | placeholder: Benefits...
20 | validations:
21 | required: true
22 | - type: textarea
23 | id: detail
24 | attributes:
25 | label: "Detail"
26 | description: How would you like the enhancement to work? Please provide as much detail as possible
27 | placeholder: Detailed description...
28 | validations:
29 | required: false
30 | - type: textarea
31 | id: examples
32 | attributes:
33 | label: "Examples"
34 | description: Are there any examples of this enhancement in other products/services? If so, please provide links or references.
35 | placeholder: Links/References...
36 | validations:
37 | required: false
38 | - type: textarea
39 | id: risks
40 | attributes:
41 | label: "Risks/Downsides"
42 | description: Do you think this enhancement could have any potential downsides or risks?
43 | placeholder: Risks/Downsides...
44 | validations:
45 | required: false
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/FEATURE-REQUEST.md:
--------------------------------------------------------------------------------
1 |
4 | ## Feedback requesting a new feature can be shared [here.](https://feedback.optimizely.com/)
5 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: 💡Feature Requests
4 | url: https://feedback.optimizely.com/
5 | about: Feedback requesting a new feature can be shared here.
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Summary
2 | - The "what"; a concise description of each logical change
3 | - Another change
4 |
5 | The "why", or other context.
6 |
7 | ## Test plan
8 |
9 | ## Issues
10 | - "THING-1234" or "Fixes #123"
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Reusable action of building snapshot and publish
2 |
3 | on:
4 | workflow_call:
5 | inputs:
6 | action:
7 | required: true
8 | type: string
9 | github_tag:
10 | required: false
11 | type: string
12 | secrets:
13 | MAVEN_SIGNING_KEY_BASE64:
14 | required: false
15 | MAVEN_SIGNING_PASSPHRASE:
16 | required: false
17 | MAVEN_CENTRAL_USERNAME:
18 | required: false
19 | MAVEN_CENTRAL_PASSWORD:
20 | required: false
21 | jobs:
22 | run_build:
23 | runs-on: ubuntu-latest
24 | steps:
25 | - uses: actions/checkout@v4
26 | - name: set up JDK 17
27 | uses: actions/setup-java@v4
28 | with:
29 | java-version: '17'
30 | distribution: 'temurin'
31 | - name: Setup Gradle cache
32 | uses: gradle/gradle-build-action@v2
33 | with:
34 | gradle-version: wrapper
35 | - name: Grant execute permission for gradlew
36 | run: chmod +x gradlew
37 | - name: Clean all modules
38 | run: ./gradlew cleanAllModules
39 | - name: ${{ inputs.action }}
40 | env:
41 | MAVEN_SIGNING_KEY_BASE64: ${{ secrets.MAVEN_SIGNING_KEY_BASE64 }}
42 | MAVEN_SIGNING_PASSPHRASE: ${{ secrets.MAVEN_SIGNING_PASSPHRASE }}
43 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
44 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
45 | run: GITHUB_TAG=${{ inputs.github_tag }} ./gradlew ${{ inputs.action }}
46 |
--------------------------------------------------------------------------------
/.github/workflows/source_clear_cron.yml:
--------------------------------------------------------------------------------
1 | name: Source clear
2 |
3 | on:
4 | schedule:
5 | # Runs "weekly"
6 | - cron: '0 0 * * 0'
7 |
8 | jobs:
9 | source_clear:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 | - name: Source clear scan
14 | env:
15 | SRCCLR_API_TOKEN: ${{ secrets.SRCCLR_API_TOKEN }}
16 | run: curl -sSL https://download.sourceclear.com/ci.sh | bash -s – scan
--------------------------------------------------------------------------------
/.github/workflows/ticket_reference_check.yml:
--------------------------------------------------------------------------------
1 | name: Jira ticket reference check
2 |
3 | on:
4 | pull_request:
5 | types: [opened, edited, reopened, synchronize]
6 |
7 | jobs:
8 |
9 | jira_ticket_reference_check:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - name: Check for Jira ticket reference
14 | uses: optimizely/github-action-ticket-reference-checker-public@master
15 | with:
16 | bodyRegex: 'FSSDK-(?\d+)'
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 |
5 | # selectively ignored (copyright must not be ignored)
6 | /.idea/*
7 | !/.idea/copyright
8 |
9 | .DS_Store
10 | /build
11 | /captures
12 | values.gradle
13 | jacoco.exec
14 |
15 |
--------------------------------------------------------------------------------
/.idea/copyright/FullStack_Copyright.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/copyright/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | # This is a comment.
2 | # Each line is a file pattern followed by one or more owners.
3 |
4 | # These owners will be the default owners for everything in the repo.
5 | # Unless a later match takes precedence, @global-owner1 and @global-owner2
6 | # will be requested for review when someone opens a pull request.
7 | * @optimizely/fullstack-devs
8 |
9 | # Order is important; the last matching pattern takes the most precedence.
10 | # When someone opens a pull request that only modifies JS files, only @js-owner
11 | # and not the global owner(s) will be requested for a review.
12 | #*.js @js-owner
13 |
14 | # You can also use email addresses if you prefer. They'll be used to look up
15 | # users just like we do for commit author emails.
16 | #docs/* docs@example.com
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to the Optimizely Android SDK
2 |
3 | We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code.
4 |
5 | ## Development process
6 |
7 | 1. Fork the repository and create your branch from master.
8 | 2. Please follow the [commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines) for each commit message.
9 | 3. Make sure to add tests!
10 | 4. `git push` your changes to GitHub.
11 | 5. Open a PR from your fork into the master branch of the original repo
12 | 6. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`.
13 | 7. Open a pull request from `YOUR_NAME/branch_name` to `master`.
14 | 8. A repository maintainer will review your pull request and, if all goes well, squash and merge it!
15 |
16 | All branches will be built and run against the entire test suite on Gtihub Actions with every commit.
17 |
18 | 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.
19 |
20 | ## Pull request acceptance criteria
21 |
22 | * **All code must have test coverage.** We use JUnit. Changes in functionality should have accompanying unit tests. Bug fixes should have accompanying regression tests.
23 | * Unit tests are located in `MODULE_NAME/src/test` and instrumented tests are located in `MODULE_NAME/src/androidTest` for each module with one file per class.
24 | * Version will be bumped automatically through Gradle after your change is merged.
25 | * Make sure `./gradlew testAllModules` runs successfully before submitting.
26 |
27 | ## Style
28 |
29 | Refer to the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html)
30 |
31 | ## License
32 |
33 | All contributions are under the CLA mentioned above. For this project, Optimizely uses the Apache 2.0 license, and so asks that by contributing your code, you agree to license your contribution under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). Your contributions should also include the following header:
34 |
35 | ```
36 | /****************************************************************************
37 | * Copyright YEAR, Optimizely, Inc. and contributors *
38 | * *
39 | * Licensed under the Apache License, Version 2.0 (the "License"); *
40 | * you may not use this file except in compliance with the License. *
41 | * You may obtain a copy of the License at *
42 | * *
43 | * http://www.apache.org/licenses/LICENSE-2.0 *
44 | * *
45 | * Unless required by applicable law or agreed to in writing, software *
46 | * distributed under the License is distributed on an "AS IS" BASIS, *
47 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
48 | * See the License for the specific language governing permissions and *
49 | * limitations under the License. *
50 | ***************************************************************************/
51 | ```
52 |
53 | The YEAR above should be the year of the contribution. If work on the file has been done over multiple years, list each year in the section above. Example: Optimizely writes the file and releases it in 2014. No changes are made in 2015. Change made in 2016. YEAR should be “2014, 2016”.
54 |
55 | ## Contact
56 |
57 | If you have questions, please contact developers@optimizely.com.
58 |
--------------------------------------------------------------------------------
/android-sdk/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyClientEngineTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017,2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.sdk;
18 |
19 | import android.app.UiModeManager;
20 | import android.content.Context;
21 | import android.content.res.Configuration;
22 | import android.os.Build;
23 | import androidx.annotation.RequiresApi;
24 | import androidx.test.ext.junit.runners.AndroidJUnit4;
25 |
26 | import com.optimizely.ab.event.internal.payload.EventBatch;
27 |
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 |
31 | import static junit.framework.Assert.assertEquals;
32 | import static org.mockito.Mockito.mock;
33 | import static org.mockito.Mockito.when;
34 |
35 | @RunWith(AndroidJUnit4.class)
36 | public class OptimizelyClientEngineTest {
37 | @Test
38 | public void testGetClientEngineNameFromContextAndroidTV() {
39 | Context context = mock(Context.class);
40 | UiModeManager uiModeManager = mock(UiModeManager.class);
41 | when(context.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(uiModeManager);
42 | when(uiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_TELEVISION);
43 | assertEquals("android-tv-sdk", OptimizelyClientEngine.getClientEngineNameFromContext(context));
44 | }
45 |
46 | @Test
47 | public void testGetClientEngineNameFromContextAndroid() {
48 | Context context = mock(Context.class);
49 | UiModeManager uiModeManager = mock(UiModeManager.class);
50 | when(context.getSystemService(Context.UI_MODE_SERVICE)).thenReturn(uiModeManager);
51 | when(uiModeManager.getCurrentModeType()).thenReturn(Configuration.UI_MODE_TYPE_NORMAL);
52 | assertEquals("android-sdk", OptimizelyClientEngine.getClientEngineNameFromContext(context));
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyDefaultAttributesTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, 2023 Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 | package com.optimizely.ab.android.sdk;
17 |
18 | import java.util.Map;
19 | import android.content.Context;
20 |
21 | import androidx.test.ext.junit.runners.AndroidJUnit4;
22 |
23 | import org.junit.After;
24 | import org.junit.Before;
25 | import org.junit.Test;
26 | import org.junit.runner.RunWith;
27 | import org.slf4j.Logger;
28 |
29 | import static org.mockito.ArgumentMatchers.any;
30 | import static org.mockito.ArgumentMatchers.eq;
31 | import static org.mockito.Mockito.mock;
32 | import static org.mockito.Mockito.verify;
33 | import static org.mockito.Mockito.when;
34 |
35 | import static org.junit.Assert.*;
36 |
37 | /**
38 | * Tests for {@link OptimizelyDefaultAttributes}
39 | */
40 | @RunWith(AndroidJUnit4.class)
41 | public class OptimizelyDefaultAttributesTest {
42 | private Logger logger;
43 |
44 | @Before
45 | public void setUp() throws Exception {
46 | logger = mock(Logger.class);
47 | }
48 |
49 | @After
50 | public void tearDown() throws Exception {
51 |
52 | }
53 |
54 | @Test
55 | public void buildDefaultAttributesMap() throws Exception {
56 | Context context = mock(Context.class);
57 | Context appContext = mock(Context.class);
58 | when(context.getApplicationContext()).thenReturn(appContext);
59 | when(appContext.getPackageName()).thenReturn("com.optly");
60 |
61 | Map defaultAttributes = OptimizelyDefaultAttributes.buildDefaultAttributesMap(context, logger);
62 |
63 | assertEquals(defaultAttributes.size(), 4);
64 | }
65 |
66 | @Test
67 | public void buildODPCommonData() throws Exception {
68 | Context context = mock(Context.class);
69 | Map commonData = OptimizelyDefaultAttributes.buildODPCommonData(context, logger);
70 |
71 | assertEquals(commonData.size(), 4);
72 |
73 | assertEquals(commonData.get("os"), "Android");
74 | assertEquals(commonData.get("device_type"), "Phone");
75 | assertTrue(commonData.get("os_version").toString().length() >= 1);
76 | assertTrue(commonData.get("model").toString().length() > 2);
77 | }
78 |
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/android-sdk/src/debug/res/raw/datafile:
--------------------------------------------------------------------------------
1 | {"groups": [], "projectId": "8504447126", "variables": [{"defaultValue": "true", "type": "boolean", "id": "8516291943", "key": "test_variable"}], "version": "3", "experiments": [{"status": "Running", "key": "android_experiment_key", "layerId": "8499056327", "trafficAllocation": [{"entityId": "8509854340", "endOfRange": 5000}, {"entityId": "8505434669", "endOfRange": 10000}], "audienceIds": [], "variations": [{"variables": [], "id": "8509854340", "key": "var_1"}, {"variables": [], "id": "8505434669", "key": "var_2"}], "forcedVariations": {}, "id": "8509139139"}], "audiences": [], "anonymizeIP": true, "attributes": [], "revision": "7", "events": [{"experimentIds": ["8509139139"], "id": "8505434668", "key": "test_event"}], "accountId": "8362480420"}
--------------------------------------------------------------------------------
/android-sdk/src/debug/res/raw/datafile_api:
--------------------------------------------------------------------------------
1 | {"version":"4","rollouts":[],"anonymizeIP":true,"botFiltering":true,"projectId":"10431130345","variables":[],"featureFlags":[{"experimentIds":["10390977673"],"id":"4482920077","key":"feature_1","rolloutId":"","variables":[{"defaultValue":"42","id":"2687470095","key":"i_42","type":"integer"},{"defaultValue":"4.2","id":"2689280165","key":"d_4_2","type":"double"},{"defaultValue":"true","id":"2689660112","key":"b_true","type":"boolean"},{"defaultValue":"foo","id":"2696150066","key":"s_foo","type":"string"}]},{"experimentIds":["10420810910"],"id":"4482920078","key":"feature_2","rolloutId":"","variables":[]}],"experiments":[{"status":"Running","key":"exp_with_audience","layerId":"10420273888","trafficAllocation":[{"entityId":"10389729780","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"featureEnabled":true,"id":"10389729780","key":"a"},{"variables":[],"id":"10416523121","key":"b"}],"forcedVariations":{},"id":"10390977673"},{"status":"Running","key":"exp_no_audience","layerId":"10417730432","trafficAllocation":[{"entityId":"10418551353","endOfRange":10000}],"audienceIds":[],"variations":[{"variables":[],"id":"10418551353","key":"variation_with_traffic"},{"variables":[],"id":"10418510624","key":"variation_no_traffic"}],"forcedVariations":{},"id":"10420810910"}],"audiences":[{"id":"10413101795","conditions":"[\"and\", [\"or\", [\"or\", {\"type\": \"custom_attribute\", \"name\": \"testvar\", \"value\": \"testvalue\"}]]]","name":"testvalue_audience"}],"groups":[],"attributes":[{"id":"10401066170","key":"testvar"}],"accountId":"10367498574","events":[{"experimentIds":["10420810910"],"id":"10404198134","key":"event1"},{"experimentIds":["10420810910","10390977673"],"id":"10404198135","key":"event_multiple_running_exp_attached"}],"revision":"241"}
--------------------------------------------------------------------------------
/android-sdk/src/debug/res/raw/emptydatafile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/android-sdk/src/debug/res/raw/emptydatafile
--------------------------------------------------------------------------------
/android-sdk/src/debug/res/raw/validprojectconfigv3:
--------------------------------------------------------------------------------
1 | { "groups": [], "projectId": "8504447126", "variables": [{ "defaultValue": "true", "type": "boolean", "id": "8516291943", "key": "test_variable" }], "version": "3", "experiments": [{ "status": "Running", "key": "android_experiment_key", "layerId": "8499056327", "trafficAllocation": [{ "entityId": "8509854340", "endOfRange": 5000 }, { "entityId": "8505434669", "endOfRange": 10000 }], "audienceIds": [], "variations": [{ "variables": [], "id": "8509854340", "key": "var_1" }, { "variables": [], "id": "8505434669", "key": "var_2" }], "forcedVariations": {}, "id": "8509139139" }], "audiences": [], "anonymizeIP": true, "attributes": [], "revision": "7", "events": [{ "experimentIds": ["8509139139"], "id": "8505434668", "key": "test_event" }], "accountId": "8362480420" }
--------------------------------------------------------------------------------
/android-sdk/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyClientEngine.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.sdk;
18 |
19 | import android.app.UiModeManager;
20 | import android.content.Context;
21 | import android.content.res.Configuration;
22 | import androidx.annotation.NonNull;
23 |
24 | import com.optimizely.ab.event.internal.payload.EventBatch;
25 |
26 | /**
27 | * This class manages client engine value of the Event depending on current mode of UI.
28 | */
29 | public class OptimizelyClientEngine {
30 |
31 | /**
32 | * Get client engine name for current UI mode type
33 | *
34 | * @param context any valid Android {@link Context}
35 | * @return client engine name ("android-sdk" or "android-tv-sdk")
36 | */
37 | public static String getClientEngineNameFromContext(@NonNull Context context) {
38 | UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
39 |
40 | if (uiModeManager != null && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
41 | return "android-tv-sdk";
42 | }
43 |
44 | return "android-sdk";
45 | }
46 |
47 | /**
48 | * Get client engine value for current UI mode type
49 | *
50 | * @param context any valid Android {@link Context}
51 | * @return String value of client engine
52 | * @deprecated Consider using {@link #getClientEngineNameFromContext(Context)}
53 | */
54 | @Deprecated
55 | public static EventBatch.ClientEngine getClientEngineFromContext(@NonNull Context context) {
56 | UiModeManager uiModeManager = (UiModeManager) context.getSystemService(Context.UI_MODE_SERVICE);
57 |
58 | if (uiModeManager != null && uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
59 | return EventBatch.ClientEngine.ANDROID_TV_SDK;
60 | }
61 |
62 | return EventBatch.ClientEngine.ANDROID_SDK;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyStartListener.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.sdk;
18 |
19 | import android.app.Activity;
20 | import android.content.Context;
21 |
22 | /**
23 | * Listens for new instances of {@link OptimizelyClient} that are built after calling
24 | *{@link OptimizelyManager#initialize(Context, Integer,OptimizelyStartListener)}
25 | */
26 | public interface OptimizelyStartListener {
27 | /**
28 | * Receives a started {@link OptimizelyClient} instances
29 | *
30 | * @param optimizely an {@link OptimizelyClient} that is started
31 | */
32 | void onStart(OptimizelyClient optimizely);
33 | }
34 |
--------------------------------------------------------------------------------
/android-sdk/src/main/resources/android-logger.properties:
--------------------------------------------------------------------------------
1 | #****************************************************************************
2 | # Copyright 2016, Optimizely, Inc. and contributors *
3 | # *
4 | # Licensed under the Apache License, Version 2.0 (the "License"); *
5 | # you may not use this file except in compliance with the License. *
6 | # You may obtain a copy of the License at *
7 | # *
8 | # http://www.apache.org/licenses/LICENSE-2.0 *
9 | # *
10 | # Unless required by applicable law or agreed to in writing, software *
11 | # distributed under the License is distributed on an "AS IS" BASIS, *
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | # See the License for the specific language governing permissions and *
14 | # limitations under the License. *
15 | #**************************************************************************/
16 |
17 | # Android Logger configuration example
18 |
19 | # https://github.com/noveogroup/android-logger
20 | # Valid logging levels are: VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT
21 |
22 | # Default configuration (disabled because it would override tags from the application)
23 | #root=DEBUG:Optly
24 |
25 | # Core
26 | logger.com.optimizely.ab.Optimizely=DEBUG:Optly.core
27 | logger.com.optimizely.ab.annotations=DEBUG:Optly.annotations
28 | logger.com.optimizely.ab.bucketing=DEBUG:Optly.bucketing
29 | logger.com.optimizely.ab.config=DEBUG:Optly.config
30 | logger.com.optimizely.ab.error=DEBUG:Optly.error
31 | logger.com.optimizely.ab.event=DEBUG:Optly.event
32 | logger.com.optimizely.ab.internal=DEBUG:Optly.internal
33 |
34 | # Android
35 | logger.com.optimizely.ab.android.sdk=DEBUG:Optly.androidSdk
36 | logger.com.optimizely.ab.android.datafile_handler=DEBUG:Optly.datafileHandler
37 | logger.com.optimizely.ab.android.event_handler=DEBUG:Optly.eventHandler
38 | logger.com.optimizely.ab.android.shared=DEBUG:Optly.shared
39 | logger.com.optimizely.ab.android.user_profile=DEBUG:Optly.userProfile
40 |
41 | # Disable most SDK logs by commenting all other lines and uncommenting below
42 | #logger.com.optimizely.ab=ASSERT:Optly
43 |
--------------------------------------------------------------------------------
/android-sdk/src/test/java/com/optimizely/ab/android/sdk/OptimizelyManagerOptlyActivityLifecycleCallbacksTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016,2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.sdk;
18 |
19 | import android.app.Activity;
20 | import android.os.Build;
21 |
22 | import org.junit.Before;
23 | import org.junit.Test;
24 | import org.junit.runner.RunWith;
25 | import org.mockito.Mock;
26 | import org.mockito.junit.MockitoJUnitRunner;
27 |
28 | import static org.mockito.Mockito.verify;
29 |
30 | /**
31 | * Tests for {@link OptimizelyManager.OptlyActivityLifecycleCallbacks}
32 | */
33 | @RunWith(MockitoJUnitRunner.class)
34 | public class OptimizelyManagerOptlyActivityLifecycleCallbacksTest {
35 |
36 | @Mock OptimizelyManager optimizelyManager;
37 | @Mock Activity activity;
38 | private OptimizelyManager.OptlyActivityLifecycleCallbacks optlyActivityLifecycleCallbacks;
39 |
40 | @Before
41 | public void setup() {
42 | optlyActivityLifecycleCallbacks = new OptimizelyManager.OptlyActivityLifecycleCallbacks(optimizelyManager);
43 | }
44 |
45 | @Test
46 | public void onActivityStopped() {
47 | optlyActivityLifecycleCallbacks.onActivityStopped(activity);
48 | verify(optimizelyManager).stop(activity, optlyActivityLifecycleCallbacks);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/datafile-handler/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/datafile-handler/build.gradle:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | apply plugin: 'com.android.library'
18 | apply plugin: 'kotlin-android'
19 |
20 | android {
21 | compileSdkVersion compile_sdk_version
22 | buildToolsVersion build_tools_version
23 |
24 | defaultConfig {
25 | minSdkVersion min_sdk_version
26 | targetSdkVersion target_sdk_version
27 | multiDexEnabled true
28 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
29 | }
30 | testOptions {
31 | unitTests.returnDefaultValues = true
32 | }
33 | buildTypes {
34 | release {
35 | minifyEnabled false
36 | }
37 | debug {
38 | testCoverageEnabled true
39 | }
40 | }
41 | compileOptions {
42 | sourceCompatibility JavaVersion.VERSION_17
43 | targetCompatibility JavaVersion.VERSION_17
44 | }
45 | }
46 |
47 | dependencies {
48 | api project(':shared')
49 | implementation "androidx.annotation:annotation:$annotations_ver"
50 | implementation "androidx.work:work-runtime:$work_runtime"
51 |
52 | compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
53 |
54 | testImplementation "junit:junit:$junit_ver"
55 | testImplementation "org.mockito:mockito-core:$mockito_ver"
56 | testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
57 |
58 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
59 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
60 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
61 | // Set this dependency to use JUnit 4 rules
62 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
63 | androidTestImplementation "androidx.test:core:$androidx_test_core"
64 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
65 |
66 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
67 | androidTestImplementation "org.mockito:mockito-android:$mockito_ver"
68 | androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
69 | androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
71 | }
72 |
--------------------------------------------------------------------------------
/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileCacheTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.datafile_handler;
18 |
19 | import androidx.test.platform.app.InstrumentationRegistry;
20 | import androidx.test.ext.junit.runners.AndroidJUnit4;
21 |
22 | import com.optimizely.ab.android.shared.Cache;
23 |
24 | import org.json.JSONException;
25 | import org.json.JSONObject;
26 | import org.junit.Before;
27 | import org.junit.Test;
28 | import org.junit.runner.RunWith;
29 | import org.slf4j.Logger;
30 |
31 | import java.io.IOException;
32 |
33 | import static junit.framework.Assert.assertEquals;
34 | import static junit.framework.Assert.assertNotNull;
35 | import static junit.framework.Assert.assertNull;
36 | import static junit.framework.Assert.assertTrue;
37 | import static org.mockito.Mockito.mock;
38 | import static org.mockito.Mockito.verify;
39 | import static org.mockito.Mockito.when;
40 | import static org.mockito.ArgumentMatchers.any;
41 | import static org.mockito.ArgumentMatchers.contains;
42 |
43 | /**
44 | * Tests for {@link DatafileCache}
45 | */
46 | @RunWith(AndroidJUnit4.class)
47 | public class DatafileCacheTest {
48 |
49 | private DatafileCache datafileCache;
50 | private Logger logger;
51 |
52 | @Before
53 | public void setup() {
54 | logger = mock(Logger.class);
55 | Cache cache = new Cache(InstrumentationRegistry.getInstrumentation().getTargetContext(), logger);
56 | datafileCache = new DatafileCache("1", cache, logger);
57 | }
58 |
59 | @Test
60 | public void loadBeforeSaving() {
61 | assertNull(datafileCache.load());
62 | }
63 |
64 | @Test
65 | public void persistence() throws JSONException {
66 | assertTrue(datafileCache.save("{}"));
67 | final JSONObject jsonObject = datafileCache.load();
68 | assertNotNull(jsonObject);
69 | final String actual = jsonObject.toString();
70 | assertEquals(new JSONObject("{}").toString(), actual);
71 | assertTrue(datafileCache.delete());
72 | assertNull(datafileCache.load());
73 | }
74 |
75 | @Test
76 | public void loadJsonException() throws IOException {
77 | Cache cache = mock(Cache.class);
78 | DatafileCache datafileCache = new DatafileCache("1", cache, logger);
79 | when(cache.load(datafileCache.getFileName())).thenReturn("{");
80 | assertNull(datafileCache.load());
81 | verify(logger).error(contains("Unable to parse data file"), any(JSONException.class));
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileWorkerTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.datafile_handler;
18 |
19 | import static org.hamcrest.CoreMatchers.is;
20 | import static org.junit.Assert.assertEquals;
21 | import static org.junit.Assert.assertThat;
22 | import static org.mockito.ArgumentMatchers.any;
23 | import static org.mockito.ArgumentMatchers.anyString;
24 | import static org.mockito.ArgumentMatchers.eq;
25 | import static org.mockito.Mockito.mock;
26 | import static org.mockito.Mockito.verify;
27 | import static org.mockito.Mockito.when;
28 |
29 | import android.content.Context;
30 |
31 | import androidx.test.core.app.ApplicationProvider;
32 | import androidx.test.ext.junit.runners.AndroidJUnit4;
33 | import androidx.work.Data;
34 | import androidx.work.ListenableWorker;
35 | import androidx.work.testing.TestWorkerBuilder;
36 |
37 | import com.optimizely.ab.android.shared.Cache;
38 | import com.optimizely.ab.android.shared.DatafileConfig;
39 | import com.optimizely.ab.event.LogEvent;
40 |
41 | import org.junit.Before;
42 | import org.junit.Test;
43 | import org.junit.runner.RunWith;
44 | import org.slf4j.Logger;
45 | import org.slf4j.LoggerFactory;
46 |
47 | import java.util.concurrent.Executor;
48 | import java.util.concurrent.Executors;
49 |
50 | /**
51 | * Tests {@link DatafileWorker}
52 | */
53 | @RunWith(AndroidJUnit4.class)
54 | public class DatafileWorkerTest {
55 | private Context context;
56 | private Executor executor;
57 | private String sdkKey = "sdkKey";
58 | private Logger logger = LoggerFactory.getLogger("test");
59 |
60 | @Before
61 | public void setUp() {
62 | context = ApplicationProvider.getApplicationContext();
63 | executor = Executors.newSingleThreadExecutor();
64 | }
65 |
66 | @Test
67 | public void testInputData() {
68 | DatafileConfig datafileConfig1 = new DatafileConfig(null, sdkKey);
69 | Data data = DatafileWorker.getData(datafileConfig1);
70 |
71 | DatafileConfig datafileConfig2 = DatafileWorker.getDataConfig(data);
72 | assertEquals(datafileConfig2.getKey(), sdkKey);
73 | }
74 |
75 | @Test
76 | public void testDatafileFetch() {
77 | DatafileWorker worker = mockDatafileWorker(sdkKey);
78 | worker.datafileLoader = mock(DatafileLoader.class);
79 |
80 | ListenableWorker.Result result = worker.doWork();
81 |
82 | DatafileConfig datafileConfig = new DatafileConfig(null, sdkKey);
83 | String datafileUrl = datafileConfig.getUrl();
84 | DatafileCache datafileCache = new DatafileCache(datafileConfig.getKey(), new Cache(context, logger), logger);
85 |
86 | verify(worker.datafileLoader).getDatafile(eq(datafileUrl), eq(datafileCache), eq(null));
87 | assertThat(result, is(ListenableWorker.Result.success())); // success
88 | }
89 |
90 | // Helpers
91 |
92 | DatafileWorker mockDatafileWorker(String sdkKey) {
93 | DatafileConfig datafileConfig = new DatafileConfig(null, sdkKey);
94 | Data inputData = DatafileWorker.getData(datafileConfig);
95 |
96 | return (DatafileWorker) TestWorkerBuilder.from(context, DatafileWorker.class, executor)
97 | .setInputData(inputData)
98 | .build();
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/datafile-handler/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileCache.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.datafile_handler;
18 |
19 | import androidx.annotation.NonNull;
20 | import androidx.annotation.Nullable;
21 | import androidx.annotation.VisibleForTesting;
22 |
23 | import com.optimizely.ab.android.shared.Cache;
24 | import com.optimizely.ab.android.shared.DatafileConfig;
25 |
26 | import org.json.JSONException;
27 | import org.json.JSONObject;
28 | import org.slf4j.Logger;
29 |
30 | /**
31 | * Abstracts the actual datafile to a cached file containing the JSONObject as a string.
32 | */
33 | public class DatafileCache {
34 |
35 | private static final String FILENAME = "optly-data-file-%s.json";
36 |
37 | @NonNull private final Cache cache;
38 | @NonNull private final String filename;
39 | @NonNull private final Logger logger;
40 |
41 | /**
42 | * Create a DatafileCache Object
43 | * @param cacheKey key used to cache
44 | * @param cache shared generic file based {link Cache}
45 | * @param logger logger to use
46 | */
47 | public DatafileCache(@NonNull String cacheKey, @NonNull Cache cache, @NonNull Logger logger) {
48 | this.cache = cache;
49 | this.filename = String.format(FILENAME, cacheKey);
50 | this.logger = logger;
51 | }
52 |
53 | /**
54 | * Delete the datafile cache
55 | * @return true if successful
56 | */
57 | public boolean delete() {
58 | return cache.delete(filename);
59 | }
60 |
61 | /**
62 | * Check to see if the datafile cache exists
63 | * @return true if it exists
64 | */
65 | public boolean exists() {
66 | return cache.exists(filename);
67 | }
68 |
69 | /**
70 | * Return the filename to the datafile cache
71 | * @return filename for datafile cache
72 | */
73 | @VisibleForTesting
74 | public String getFileName() {
75 | return filename;
76 | }
77 |
78 | /**
79 | * Loads the datafile from cache into a JSONObject
80 | * @return JSONObject if exists or null if it doesn't or there was a problem
81 | */
82 | @Nullable
83 | public JSONObject load() {
84 | String datafile = cache.load(filename);
85 |
86 | if (datafile == null) {
87 | return null;
88 | }
89 | try {
90 | return new JSONObject(datafile);
91 | } catch (JSONException e) {
92 | logger.error("Unable to parse data file", e);
93 | return null;
94 | }
95 | }
96 |
97 | /**
98 | * Save a datafile to cache.
99 | * @param dataFile to write to cache
100 | * @return true if successful.
101 | */
102 | public boolean save(String dataFile) {
103 | return cache.save(filename, dataFile);
104 | }
105 |
106 | public boolean equals(Object o) {
107 | if (o == this) {
108 | return true;
109 | }
110 | if (!(o instanceof DatafileCache)) {
111 | return false;
112 | }
113 | DatafileCache p = (DatafileCache) o;
114 | return this.filename.equals(p.filename);
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoadedListener.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.datafile_handler;
18 |
19 | import androidx.annotation.Nullable;
20 |
21 | /**
22 | * Listens for new Optimizely datafiles
23 | *
24 | * Datafiles can come from a local file or the CDN
25 | *
26 | *
27 | */
28 | public interface DatafileLoadedListener {
29 |
30 | /**
31 | * Called with new datafile after a download.
32 | *
33 | * @param dataFile the datafile json, can be null if datafile loading failed.
34 | *
35 | */
36 | void onDatafileLoaded(@Nullable String dataFile);
37 | }
38 |
--------------------------------------------------------------------------------
/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileWorker.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 | package com.optimizely.ab.android.datafile_handler;
17 |
18 | import android.content.Context;
19 |
20 | import androidx.annotation.NonNull;
21 | import androidx.annotation.VisibleForTesting;
22 | import androidx.work.Data;
23 | import androidx.work.Worker;
24 | import androidx.work.WorkerParameters;
25 |
26 | import com.optimizely.ab.android.shared.Cache;
27 | import com.optimizely.ab.android.shared.Client;
28 | import com.optimizely.ab.android.shared.DatafileConfig;
29 | import com.optimizely.ab.android.shared.OptlyStorage;
30 |
31 | import org.slf4j.LoggerFactory;
32 |
33 | public class DatafileWorker extends Worker {
34 | public static final String workerId = "DatafileWorker";
35 |
36 | @VisibleForTesting
37 | public DatafileLoader datafileLoader;
38 |
39 | public DatafileWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
40 | super(context, workerParams);
41 |
42 | DatafileClient datafileClient = new DatafileClient(
43 | new Client(new OptlyStorage(context), LoggerFactory.getLogger(OptlyStorage.class)),
44 | LoggerFactory.getLogger(DatafileClient.class));
45 | DatafileCache datafileCache = null;
46 |
47 | datafileLoader = new DatafileLoader(context, datafileClient, datafileCache, LoggerFactory.getLogger(DatafileLoader.class));
48 | }
49 |
50 | public static Data getData(DatafileConfig datafileConfig) {
51 | return new Data.Builder().putString("DatafileConfig", datafileConfig.toJSONString()).build();
52 | }
53 |
54 | public static DatafileConfig getDataConfig(Data data) {
55 | String extraDatafileConfig = data.getString("DatafileConfig");
56 | return DatafileConfig.fromJSONString(extraDatafileConfig);
57 | }
58 |
59 | @NonNull
60 | @Override
61 | public Result doWork() {
62 | DatafileConfig datafileConfig = getDataConfig(getInputData());
63 | String datafileUrl = datafileConfig.getUrl();
64 |
65 | DatafileCache datafileCache = new DatafileCache(
66 | datafileConfig.getKey(),
67 | new Cache(this.getApplicationContext(), LoggerFactory.getLogger(Cache.class)),
68 | LoggerFactory.getLogger(DatafileCache.class));
69 |
70 | datafileLoader.getDatafile(datafileUrl, datafileCache, null);
71 |
72 | return Result.success();
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/datafile-handler/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | datafile-handler
3 |
4 |
--------------------------------------------------------------------------------
/datafile-handler/src/test/java/com/optimizely/ab/android/datafile_handler/DefaultDatafileHandlerUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.optimizely.ab.android.datafile_handler;
2 |
3 | import android.content.Context;
4 |
5 | import com.optimizely.ab.android.shared.DatafileConfig;
6 |
7 | import org.junit.Test;
8 |
9 | import static org.junit.Assert.assertFalse;
10 | import static org.mockito.Mockito.mock;
11 | import static org.mockito.Mockito.when;
12 |
13 | /**
14 | * Example local unit test, which will execute on the development machine (host).
15 | *
16 | * @see Testing documentation
17 | */
18 | public class DefaultDatafileHandlerUnitTest {
19 |
20 | DatafileHandler handler = mock(DefaultDatafileHandler.class);
21 |
22 | @Test
23 | public void testHandler() throws Exception {
24 | handler = new DefaultDatafileHandler();
25 | Context context = mock(Context.class);
26 | when(context.getApplicationContext()).thenReturn(context);
27 | assertFalse(handler.isDatafileSaved(context, new DatafileConfig("1", null)));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/docs/readme.md:
--------------------------------------------------------------------------------
1 | # Generate Code Documentation
2 | ### Steps
3 |
4 | * Checkout **master** branch.
5 | * Open project in **Android Studio**.
6 | * Go to **Tools -> Generate JavaDoc**
7 | * Make sure **Generate JavaDoc scope -> Custom Scope** is checked.
8 | * Click on three dotted icon to make custom scope.
9 | * Give name **Custom_Docs**.
10 | * Copy and paste following regex: **`!test:*..*&&!lib:*..*&&!src[test-app]:*..*`** into **Pattern** and click **OK**.
11 | * Uncheck **Include test sources**.
12 | * Select **Output Directory**.
13 | * Click **OK**.
14 | * This will generate HTML documentation in given **Output Directory**.
15 | * Browse **Output Directory/index.html** in browser.
16 |
--------------------------------------------------------------------------------
/event-handler/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/event-handler/build.gradle:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | apply plugin: 'com.android.library'
18 | apply plugin: 'kotlin-android'
19 |
20 | android {
21 | compileSdkVersion compile_sdk_version
22 | buildToolsVersion build_tools_version
23 |
24 | defaultConfig {
25 | minSdkVersion min_sdk_version
26 | targetSdkVersion target_sdk_version
27 | multiDexEnabled true
28 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
29 | }
30 | testOptions {
31 | unitTests.returnDefaultValues = true
32 | unitTests.all {
33 | jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
34 | jvmArgs '--add-opens=java.base/java.util.zip=ALL-UNNAMED'
35 | jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
36 | }
37 | }
38 | buildTypes {
39 | release {
40 | minifyEnabled false
41 | }
42 | debug {
43 | testCoverageEnabled true
44 | }
45 | }
46 | compileOptions {
47 | sourceCompatibility JavaVersion.VERSION_17
48 | targetCompatibility JavaVersion.VERSION_17
49 | }
50 | buildToolsVersion build_tools_version
51 | }
52 |
53 | dependencies {
54 | api project(':shared')
55 | implementation "androidx.annotation:annotation:$annotations_ver"
56 | implementation "androidx.work:work-runtime:$work_runtime"
57 |
58 | compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
59 |
60 | testImplementation "junit:junit:$junit_ver"
61 | testImplementation "org.mockito:mockito-core:$mockito_ver"
62 | testImplementation "org.mockito:mockito-inline:$mockito_ver"
63 | testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
64 |
65 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
66 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
67 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
68 | // Set this dependency to use JUnit 4 rules
69 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
70 | androidTestImplementation "androidx.test:core:$androidx_test_core"
71 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
72 |
73 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
74 | androidTestImplementation "org.mockito:mockito-android:$mockito_ver"
75 | androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
76 | androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
77 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
78 | }
79 |
80 |
--------------------------------------------------------------------------------
/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/DefaultEventHandlerTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import android.content.Context;
20 |
21 | import androidx.test.ext.junit.runners.AndroidJUnit4;
22 | import androidx.test.platform.app.InstrumentationRegistry;
23 |
24 | import com.optimizely.ab.event.LogEvent;
25 | import com.optimizely.ab.event.internal.payload.EventBatch;
26 |
27 | import org.junit.Before;
28 | import org.junit.Test;
29 | import org.junit.runner.RunWith;
30 | import org.slf4j.Logger;
31 |
32 | import java.net.MalformedURLException;
33 | import java.util.HashMap;
34 |
35 | import static org.mockito.Mockito.mock;
36 | import static org.mockito.Mockito.verify;
37 |
38 | /**
39 | * Tests {@link DefaultEventHandler}
40 | */
41 | @RunWith(AndroidJUnit4.class)
42 | public class DefaultEventHandlerTest {
43 | private Context context;
44 | private Logger logger;
45 |
46 | private DefaultEventHandler eventHandler;
47 | private String url = "http://www.foo.com";
48 |
49 | @Before
50 | public void setupEventHandler() {
51 | context = InstrumentationRegistry.getInstrumentation().getTargetContext();
52 | logger = mock(Logger.class);
53 | eventHandler = DefaultEventHandler.getInstance(context);
54 | eventHandler.logger = logger;
55 | }
56 |
57 | @Test
58 | public void dispatchEventSuccess() throws MalformedURLException {
59 | eventHandler.dispatchEvent(new LogEvent(LogEvent.RequestMethod.POST, url, new HashMap(), new EventBatch()));
60 | verify(logger).info("Sent url {} to the event handler service", "http://www.foo.com");
61 | }
62 |
63 | @Test
64 | public void dispatchEmptyUrlString() {
65 | eventHandler.dispatchEvent(new LogEvent(LogEvent.RequestMethod.POST, "", new HashMap(), new EventBatch()));
66 | verify(logger).error("Event dispatcher received an empty url");
67 | }
68 |
69 | @Test
70 | public void dispatchEmptyParams() {
71 | eventHandler.setDispatchInterval(60L * 1000L);
72 |
73 | eventHandler.dispatchEvent(new LogEvent(LogEvent.RequestMethod.POST, url, new HashMap(), new EventBatch()));
74 | //verify(context).startService(any(Intent.class));
75 | verify(logger).info("Sent url {} to the event handler service (with retry interval of {} seconds)", "http://www.foo.com", 60L);
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventClientTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import androidx.annotation.RequiresApi;
20 | import androidx.test.ext.junit.runners.AndroidJUnit4;
21 |
22 | import com.optimizely.ab.android.shared.Client;
23 |
24 | import org.junit.Before;
25 | import org.junit.Test;
26 | import org.junit.runner.RunWith;
27 | import org.mockito.ArgumentCaptor;
28 | import org.mockito.Mock;
29 | import org.slf4j.Logger;
30 |
31 | import java.io.IOException;
32 | import java.io.InputStream;
33 | import java.io.OutputStream;
34 | import java.net.HttpURLConnection;
35 | import java.net.URL;
36 |
37 | import static org.mockito.ArgumentMatchers.any;
38 | import static org.mockito.ArgumentMatchers.anyInt;
39 | import static org.mockito.ArgumentMatchers.contains;
40 | import static org.mockito.Mockito.doThrow;
41 | import static org.mockito.Mockito.mock;
42 | import static org.mockito.Mockito.verify;
43 | import static org.mockito.Mockito.when;
44 |
45 | /**
46 | * Tests {@link DefaultEventHandler}
47 | */
48 | @RunWith(AndroidJUnit4.class)
49 | public class EventClientTest {
50 |
51 | @Mock
52 | Logger logger = mock(Logger.class);
53 | @Mock
54 | Client client = mock(Client.class);
55 | private HttpURLConnection urlConnection;
56 | private EventClient eventClient;
57 | private Event event;
58 |
59 | @Before
60 | public void setupEventClient() throws IOException {
61 | urlConnection = mock(HttpURLConnection.class);
62 | when(urlConnection.getOutputStream()).thenReturn(mock(OutputStream.class));
63 | when(urlConnection.getInputStream()).thenReturn(mock(InputStream.class));
64 | this.eventClient = new EventClient(client, logger);
65 | URL url = new URL("http://www.foo.com");
66 | event = new Event(url, "");
67 | }
68 |
69 | @Test
70 | public void testEventClient() {
71 | eventClient.sendEvent(event);
72 |
73 | verify(logger).debug("SendEvent completed: {}", event);
74 | }
75 |
76 | @Test
77 | public void testConnectionAndReadTimeout() throws IOException {
78 | when(client.openConnection(event.getURL())).thenReturn(urlConnection);
79 | when(urlConnection.getResponseCode()).thenReturn(200);
80 | doThrow(new IOException()).when(urlConnection).getOutputStream();
81 |
82 | eventClient.sendEvent(event);
83 |
84 |
85 | ArgumentCaptor captor1 = ArgumentCaptor.forClass(Client.Request.class);
86 | verify(client).execute(captor1.capture(), anyInt(), anyInt());
87 | Object response = captor1.getValue().execute();
88 |
89 | verify(logger).error(contains("Unable to send event"), any(Event.class), any(IOException.class));
90 | verify(urlConnection).setConnectTimeout(10*1000);
91 | verify(urlConnection).setReadTimeout(60*1000);
92 | verify(urlConnection).disconnect();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventReschedulerTest.java:
--------------------------------------------------------------------------------
1 | // Copyright 2016,2021,2023, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package com.optimizely.ab.android.event_handler;
16 |
17 | import android.content.Context;
18 | import android.content.Intent;
19 | import android.net.NetworkInfo;
20 | import android.net.wifi.WifiManager;
21 |
22 | import org.junit.Before;
23 | import org.junit.Ignore;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.mockito.junit.MockitoJUnitRunner;
27 | import org.slf4j.Logger;
28 |
29 | import static org.mockito.ArgumentMatchers.any;
30 | import static org.mockito.ArgumentMatchers.matches;
31 | import static org.mockito.Mockito.doNothing;
32 | import static org.mockito.Mockito.doReturn;
33 | import static org.mockito.Mockito.doThrow;
34 | import static org.mockito.Mockito.mock;
35 | import static org.mockito.Mockito.spy;
36 | import static org.mockito.Mockito.verify;
37 | import static org.mockito.Mockito.when;
38 |
39 | import androidx.test.ext.junit.runners.AndroidJUnit4;
40 |
41 | /**
42 | * Unit tests for {@link EventRescheduler}
43 | */
44 | @RunWith(AndroidJUnit4.class)
45 | public class EventReschedulerTest {
46 |
47 | private Context context;
48 | private Intent intent;
49 | private Logger logger;
50 | private EventRescheduler rescheduler;
51 |
52 | @Before
53 | public void setupEventRescheduler() {
54 | context = mock(Context.class);
55 | intent = mock(Intent.class);
56 | logger = mock(Logger.class);
57 | rescheduler = spy(new EventRescheduler());
58 | rescheduler.logger = logger;
59 | }
60 |
61 | @Test
62 | public void onReceiveNullIntent() {
63 | rescheduler.onReceive(context, null);
64 | verify(logger).warn("Received invalid broadcast to event rescheduler");
65 | }
66 |
67 | @Test
68 | public void onReceiveNullContext() {
69 | rescheduler.onReceive(null, intent);
70 | verify(logger).warn("Received invalid broadcast to event rescheduler");
71 | }
72 |
73 | @Test
74 | public void onReceiveInvalidAction() {
75 | when(intent.getAction()).thenReturn("invalid");
76 | rescheduler.onReceive(context, intent);
77 | verify(logger).warn("Received unsupported broadcast action to event rescheduler");
78 | }
79 |
80 | @Test
81 | public void onReceiveWhenRescheduleWithException() {
82 | when(intent.getAction()).thenThrow(new IllegalStateException());
83 | rescheduler.onReceive(context, intent);
84 | verify(logger).warn(matches("WorkScheduler failed to reschedule an event service.*"));
85 | }
86 |
87 | @Test
88 | public void onReceiveValidBootComplete() {
89 | when(intent.getAction()).thenReturn(Intent.ACTION_BOOT_COMPLETED);
90 | rescheduler.onReceive(context, intent);
91 | verify(logger).info("Rescheduling event flushing if necessary");
92 | }
93 |
94 | @Test
95 | public void onReceiveValidPackageReplaced() {
96 | when(intent.getAction()).thenReturn(Intent.ACTION_MY_PACKAGE_REPLACED);
97 | rescheduler.onReceive(context, intent);
98 | verify(logger).info("Rescheduling event flushing if necessary");
99 | }
100 |
101 | @Test
102 | public void flushOnWifiConnectionIfScheduled() {
103 | final Intent eventServiceIntent = mock(Intent.class);
104 | when(intent.getAction()).thenReturn(WifiManager.WIFI_STATE_CHANGED_ACTION);
105 | NetworkInfo info = mock(NetworkInfo.class);
106 | when(info.isConnected()).thenReturn(true);
107 | when(intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO)).thenReturn(info);
108 |
109 | rescheduler.reschedule(context, intent);
110 | verify(logger).info("Preemptively flushing events since wifi became available");
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/EventSQLiteOpenHelperTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016,2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import android.content.Context;
20 | import android.database.Cursor;
21 | import android.database.sqlite.SQLiteDatabase;
22 | import android.os.Build;
23 | import androidx.annotation.RequiresApi;
24 | import androidx.test.platform.app.InstrumentationRegistry;
25 | import androidx.test.ext.junit.runners.AndroidJUnit4;
26 |
27 | import org.junit.After;
28 | import org.junit.Before;
29 | import org.junit.Test;
30 | import org.junit.runner.RunWith;
31 | import org.slf4j.Logger;
32 |
33 | import static junit.framework.Assert.assertEquals;
34 | import static org.mockito.Mockito.mock;
35 | import static org.mockito.Mockito.verify;
36 |
37 | /**
38 | * Tests for {@link EventSQLiteOpenHelper}
39 | */
40 | @RunWith(AndroidJUnit4.class)
41 | public class EventSQLiteOpenHelperTest {
42 |
43 | private Context context;
44 | private Logger logger;
45 |
46 | @Before
47 | public void setup() {
48 | context = InstrumentationRegistry.getInstrumentation().getTargetContext();
49 | logger = mock(Logger.class);
50 | }
51 |
52 | @After
53 | public void teardown() {
54 | context.deleteDatabase(String.format(EventSQLiteOpenHelper.DB_NAME, "1"));
55 | }
56 |
57 | @Test
58 | public void onCreateMakesTables() {
59 | EventSQLiteOpenHelper eventSQLiteOpenHelper =
60 | new EventSQLiteOpenHelper(context, "1", null, 1, logger);
61 | SQLiteDatabase db = eventSQLiteOpenHelper.getWritableDatabase();
62 | String[] projection = {
63 | EventTable.Column._ID,
64 | EventTable.Column.URL,
65 | };
66 | Cursor cursor = db.query(EventTable.NAME, projection, null, null, null, null, null);
67 | assertEquals(2, cursor.getColumnCount());
68 | cursor.close();
69 |
70 | verify(logger).info("Created event table with SQL: {}", EventSQLiteOpenHelper.SQL_CREATE_EVENT_TABLE);
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/event-handler/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/event-handler/src/main/java/com/optimizely/ab/android/event_handler/Event.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import java.net.URL;
20 |
21 | /**
22 | * Event model
23 | */
24 | class Event {
25 | private final URL url;
26 | private final String requestBody;
27 |
28 | Event(URL url, String requestBody) {
29 | this.url = url;
30 | this.requestBody = requestBody;
31 | }
32 |
33 | String getRequestBody() {
34 | return this.requestBody;
35 | }
36 |
37 | URL getURL() {
38 | return this.url;
39 | }
40 |
41 | /**
42 | * Returns the String representation of an event
43 | *
44 | * @hide
45 | */
46 | @Override
47 | public String toString() {
48 | return String.format("Sending %s to %s", requestBody, url);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventSQLiteOpenHelper.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016,2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import android.content.Context;
20 | import android.database.sqlite.SQLiteDatabase;
21 | import android.database.sqlite.SQLiteOpenHelper;
22 | import android.os.Build;
23 | import androidx.annotation.NonNull;
24 | import androidx.annotation.Nullable;
25 | import androidx.annotation.RequiresApi;
26 |
27 | import org.slf4j.Logger;
28 |
29 | /**
30 | * Handles transactions for the events db
31 | */
32 | class EventSQLiteOpenHelper extends SQLiteOpenHelper {
33 |
34 | static final int VERSION = 1;
35 | static final String DB_NAME = "optly-events-%s";
36 |
37 | static final String SQL_CREATE_EVENT_TABLE =
38 | "CREATE TABLE " + EventTable.NAME + " (" +
39 | EventTable._ID + " INTEGER PRIMARY KEY, " +
40 | EventTable.Column.URL + " TEXT NOT NULL," +
41 | EventTable.Column.REQUEST_BODY + " TEXT NOT NULL" +
42 | ")";
43 |
44 | private static final String SQL_DELETE_EVENT_TABLE =
45 | "DROP TABLE IF EXISTS " + EventTable.NAME;
46 |
47 | @NonNull private final Logger logger;
48 | @NonNull private final String projectId;
49 | @NonNull private final Context context;
50 |
51 | EventSQLiteOpenHelper(@NonNull Context context, @NonNull String projectId, @Nullable SQLiteDatabase.CursorFactory factory, int version, @NonNull Logger logger) {
52 | super(context, String.format(DB_NAME, projectId), factory, version);
53 | this.logger = logger;
54 | this.projectId = projectId;
55 | this.context = context;
56 | }
57 |
58 | /**
59 | * @hide
60 | * @see SQLiteOpenHelper#onCreate(SQLiteDatabase)
61 | */
62 | @Override
63 | public void onCreate(SQLiteDatabase db) {
64 | try {
65 | // Deletes the old events db that stored events for all projects
66 | context.deleteDatabase("optly-events");
67 | db.execSQL(SQL_CREATE_EVENT_TABLE);
68 | logger.info("Created event table with SQL: {}", SQL_CREATE_EVENT_TABLE);
69 | } catch (Exception e) {
70 | logger.error("Error creating optly-events table.", e);
71 | }
72 | }
73 |
74 | /**
75 | * @hide
76 | * @see SQLiteOpenHelper#onUpgrade(SQLiteDatabase, int, int)
77 | */
78 |
79 | @Override
80 | public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
81 |
82 | }
83 |
84 | public String getDbName() {
85 | return String.format(DB_NAME, projectId);
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventTable.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.event_handler;
18 |
19 | import android.provider.BaseColumns;
20 |
21 | /**
22 | * Constants for Event SQL table
23 | */
24 | final class EventTable implements BaseColumns {
25 | static final String NAME = "event";
26 |
27 | private EventTable() {
28 | }
29 |
30 | class Column {
31 | static final String _ID = BaseColumns._ID;
32 | static final String URL = "url";
33 | static final String REQUEST_BODY = "requestBody";
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/event-handler/src/test/resources/org.mockito.plugins.MockMaker:
--------------------------------------------------------------------------------
1 | mock-maker-inline
2 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | android.enableJetifier=true
2 | android.useAndroidX=true
3 | org.gradle.jvmargs=-Xmx1g
4 | android.disableAutomaticComponentCreation=true
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Jan 28 11:38:35 PST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | org.gradle.jvmargs=-Xmx1g
6 | zipStorePath=wrapper/dists
7 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
8 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/odp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/odp/build.gradle:
--------------------------------------------------------------------------------
1 | //
2 | // Copyright 2022, Optimizely, Inc. and contributors
3 | //
4 | // Licensed under the Apache License, Version 2.0 (the "License");
5 | // you may not use this file except in compliance with the License.
6 | // You may obtain a copy of the License at
7 | //
8 | // https://www.apache.org/licenses/LICENSE-2.0
9 | //
10 | // Unless required by applicable law or agreed to in writing, software
11 | // distributed under the License is distributed on an "AS IS" BASIS,
12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | // See the License for the specific language governing permissions and
14 | // limitations under the License.
15 | //
16 |
17 | apply plugin: 'com.android.library'
18 | apply plugin: 'kotlin-android'
19 | apply plugin: 'org.jlleitschuh.gradle.ktlint'
20 |
21 | android {
22 | compileSdkVersion compile_sdk_version
23 | buildToolsVersion build_tools_version
24 |
25 | defaultConfig {
26 | minSdkVersion min_sdk_version
27 | targetSdkVersion target_sdk_version
28 | versionCode 1
29 | versionName "1.0"
30 | multiDexEnabled true
31 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
32 | }
33 | testOptions {
34 | unitTests.returnDefaultValues = true
35 | }
36 | buildTypes {
37 | release {
38 | minifyEnabled false
39 | }
40 | debug {
41 | testCoverageEnabled true
42 | }
43 | }
44 | compileOptions {
45 | sourceCompatibility JavaVersion.VERSION_17
46 | targetCompatibility JavaVersion.VERSION_17
47 | }
48 | buildToolsVersion build_tools_version
49 |
50 | kotlinOptions {
51 | jvmTarget = '1.8'
52 | }
53 | }
54 |
55 | dependencies {
56 | api project(':shared')
57 | implementation "androidx.annotation:annotation:$annotations_ver"
58 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
59 | implementation "androidx.work:work-runtime:$work_runtime"
60 | // Add SLF4J API for Logger interface
61 | implementation "org.slf4j:slf4j-api:$slf4j_ver"
62 |
63 | testImplementation "junit:junit:$junit_ver"
64 | testImplementation "org.mockito:mockito-core:$mockito_ver"
65 |
66 | compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
67 |
68 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
69 | androidTestImplementation "androidx.test.ext:junit-ktx:$androidx_test_junit"
70 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
71 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
72 | // Set this dependency to use JUnit 4 rules
73 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
74 | androidTestImplementation "androidx.test:core:$androidx_test_core"
75 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
76 |
77 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
78 | androidTestImplementation "org.mockito:mockito-android:$mockito_ver"
79 | androidTestImplementation "org.slf4j:slf4j-api:$slf4j_ver"
80 | }
81 |
--------------------------------------------------------------------------------
/odp/src/androidTest/java/com/optimizely/ab/android/odp/DefaultODPApiManagerTest.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2023, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package com.optimizely.ab.android.odp
16 |
17 | import android.content.Context
18 | import androidx.test.core.app.ApplicationProvider
19 | import androidx.test.ext.junit.runners.AndroidJUnit4
20 | import org.junit.Assert.assertEquals
21 | import org.junit.Test
22 | import org.junit.runner.RunWith
23 | import org.mockito.Mockito.mock
24 | import org.mockito.Mockito.verify
25 |
26 | @RunWith(AndroidJUnit4::class)
27 | class DefaultODPApiManagerTest {
28 | private val apiKey = "valid-key"
29 | private val apiEndpoint = "http://valid-endpoint"
30 |
31 | private var context: Context = ApplicationProvider.getApplicationContext()
32 | private val defaultODPApiManager = DefaultODPApiManager(context, 10, 10)
33 |
34 | @Test
35 | fun fetchQualifiedSegments() {
36 | val userKey = "fs_user_id"
37 | val userValue = "user123"
38 | val segmentsToCheck = setOf("a")
39 |
40 | val expRequestPayload = "{\"query\": \"query(\$userId: String, \$audiences: [String]) {customer(fs_user_id: \$userId) {audiences(subset: \$audiences) {edges {node {name state}}}}}\", \"variables\": {\"userId\": \"user123\", \"audiences\": [\"a\"]}}"
41 |
42 | val segmentClient = mock(ODPSegmentClient::class.java)
43 | defaultODPApiManager.segmentClient = segmentClient
44 |
45 | defaultODPApiManager.fetchQualifiedSegments(apiKey, apiEndpoint, userKey, userValue, segmentsToCheck)
46 | verify(segmentClient).fetchQualifiedSegments(apiKey, apiEndpoint, expRequestPayload)
47 | }
48 |
49 | @Test
50 | fun sendEvents() {
51 | val payload = "event-payload"
52 | val status = defaultODPApiManager.sendEvents(apiKey, apiEndpoint, payload)
53 | assertEquals(status, 200) // always return success to java-sdk
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/odp/src/androidTest/java/com/optimizely/ab/android/odp/ODPSegmentClientTest.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2022, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package com.optimizely.ab.android.odp
16 |
17 | import androidx.test.ext.junit.runners.AndroidJUnit4
18 | import com.optimizely.ab.android.shared.Client
19 | import java.io.OutputStream
20 | import java.net.HttpURLConnection
21 | import org.junit.Assert.assertNull
22 | import org.junit.Before
23 | import org.junit.Test
24 | import org.junit.runner.RunWith
25 | import org.mockito.ArgumentCaptor
26 | import org.mockito.ArgumentMatchers.any
27 | import org.mockito.ArgumentMatchers.contains
28 | import org.mockito.ArgumentMatchers.eq
29 | import org.mockito.Mockito.`when`
30 | import org.mockito.Mockito.mock
31 | import org.mockito.Mockito.verify
32 | import org.slf4j.Logger
33 |
34 | @RunWith(AndroidJUnit4::class)
35 | class ODPSegmentClientTest {
36 | private val logger = mock(Logger::class.java)
37 | private val client = mock(Client::class.java)
38 | private val urlConnection = mock(HttpURLConnection::class.java)
39 | private val captor = ArgumentCaptor.forClass(Client.Request::class.java)
40 | private lateinit var segmentClient: ODPSegmentClient
41 |
42 | private val apiKey = "valid-key"
43 | private var apiEndpoint = "http://valid-endpoint"
44 | private val payload = "valid-payload"
45 | private val response = "valid-response"
46 |
47 | @Before
48 | fun setUp() {
49 | segmentClient = ODPSegmentClient(client, logger)
50 | `when`(client.openConnection(any())).thenReturn(urlConnection)
51 | `when`(client.readStream(urlConnection)).thenReturn(response)
52 | `when`(urlConnection.outputStream).thenReturn(mock(OutputStream::class.java))
53 | }
54 |
55 | @Test
56 | fun fetchQualifiedSegments_200() {
57 | `when`(urlConnection.responseCode).thenReturn(200)
58 |
59 | segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload)
60 |
61 | verify(client).execute(captor.capture(), eq(0), eq(0))
62 | val received = captor.value.execute()
63 |
64 | assert(received == response)
65 | verify(urlConnection).connectTimeout = 10 * 1000
66 | verify(urlConnection).readTimeout = 60 * 1000
67 | verify(urlConnection).setRequestProperty("x-api-key", apiKey)
68 | verify(urlConnection).disconnect()
69 | }
70 |
71 | @Test
72 | fun fetchQualifiedSegments_400() {
73 | `when`(urlConnection.responseCode).thenReturn(400)
74 |
75 | segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload)
76 |
77 | verify(client).execute(captor.capture(), eq(0), eq(0))
78 | val received = captor.value.execute()
79 |
80 | assertNull(received)
81 | verify(logger).error("Unexpected response from ODP segment endpoint, status: 400")
82 | verify(urlConnection).disconnect()
83 | }
84 |
85 | @Test
86 | fun fetchQualifiedSegments_500() {
87 | `when`(urlConnection.responseCode).thenReturn(500)
88 |
89 | segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload)
90 |
91 | verify(client).execute(captor.capture(), eq(0), eq(0))
92 | val received = captor.value.execute()
93 |
94 | assertNull(received)
95 | verify(logger).error("Unexpected response from ODP segment endpoint, status: 500")
96 | verify(urlConnection).disconnect()
97 | }
98 |
99 | @Test
100 | fun fetchQualifiedSegments_connectionFailed() {
101 | `when`(urlConnection.responseCode).thenReturn(200)
102 |
103 | apiEndpoint = "invalid-url"
104 | segmentClient.fetchQualifiedSegments(apiKey, apiEndpoint, payload)
105 |
106 | verify(client).execute(captor.capture(), eq(0), eq(0))
107 | val received = captor.value.execute()
108 |
109 | assertNull(received)
110 | verify(logger).error(contains("Error making ODP segment request"), any())
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/odp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
21 |
22 |
--------------------------------------------------------------------------------
/odp/src/main/java/com/optimizely/ab/android/odp/DefaultODPApiManager.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2022-2023, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package com.optimizely.ab.android.odp
16 |
17 | import android.content.Context
18 | import androidx.annotation.VisibleForTesting
19 | import com.optimizely.ab.android.shared.Client
20 | import com.optimizely.ab.android.shared.OptlyStorage
21 | import com.optimizely.ab.android.shared.WorkerScheduler
22 | import com.optimizely.ab.odp.ODPApiManager
23 | import org.slf4j.LoggerFactory
24 | import java.util.concurrent.TimeUnit
25 |
26 | @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
27 | open class DefaultODPApiManager(private val context: Context, timeoutForSegmentFetch: Int, timeoutForEventDispatch: Int) : ODPApiManager {
28 |
29 | init {
30 | ODPSegmentClient.CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(timeoutForSegmentFetch.toLong()).toInt()
31 | ODPEventClient.CONNECTION_TIMEOUT = TimeUnit.SECONDS.toMillis(timeoutForEventDispatch.toLong()).toInt()
32 | }
33 |
34 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
35 | var segmentClient = ODPSegmentClient(
36 | Client(OptlyStorage(context), LoggerFactory.getLogger(Client::class.java)),
37 | LoggerFactory.getLogger(ODPSegmentClient::class.java)
38 | )
39 | private val logger = LoggerFactory.getLogger(DefaultODPApiManager::class.java)
40 |
41 | override fun fetchQualifiedSegments(
42 | apiKey: String,
43 | apiEndpoint: String,
44 | userKey: String,
45 | userValue: String,
46 | segmentsToCheck: Set,
47 | ): List? {
48 | return segmentClient.fetchQualifiedSegments(
49 | apiKey,
50 | apiEndpoint,
51 | getSegmentsQueryPayload(userKey, userValue, segmentsToCheck)
52 | )
53 | }
54 |
55 | override fun sendEvents(apiKey: String, apiEndpoint: String, payload: String): Int {
56 | val inputData = ODPEventWorker.getData(apiKey, apiEndpoint, payload)
57 | WorkerScheduler.startService(
58 | context,
59 | ODPEventWorker.workerId,
60 | ODPEventWorker::class.java,
61 | inputData,
62 | 0
63 | )
64 |
65 | logger.debug("Sent an ODP event ({}) to the event handler service: {}", payload, apiEndpoint)
66 |
67 | // return success to the caller - retries on failure will be taken care in WorkManager.
68 | return 200
69 | }
70 |
71 | // Helpers
72 |
73 | private fun getSegmentsQueryPayload(userKey: String, userValue: String, segmentsToCheck: Set): String {
74 | val query = java.lang.String.format(
75 | "query(\$userId: String, \$audiences: [String]) {customer(%s: \$userId) {audiences(subset: \$audiences) {edges {node {name state}}}}}",
76 | userKey
77 | )
78 |
79 | val variables = java.lang.String.format(
80 | "{\"userId\": \"%s\", \"audiences\": [%s]}",
81 | userValue,
82 | getSegmentsStringForRequest(segmentsToCheck)
83 | )
84 |
85 | return String.format("{\"query\": \"%s\", \"variables\": %s}", query, variables)
86 | }
87 |
88 | private fun getSegmentsStringForRequest(segmentsList: Set): String {
89 | val segmentsString = StringBuilder()
90 | val segmentsListIterator = segmentsList.iterator()
91 | for (i in segmentsList.indices) {
92 | if (i > 0) {
93 | segmentsString.append(", ")
94 | }
95 | segmentsString.append("\"").append(segmentsListIterator.next()).append("\"")
96 | }
97 | return segmentsString.toString()
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/odp/src/main/java/com/optimizely/ab/android/odp/ODPEventWorker.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2022, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 | package com.optimizely.ab.android.odp
15 |
16 | import android.content.Context
17 | import androidx.annotation.VisibleForTesting
18 | import androidx.work.Data
19 | import androidx.work.Worker
20 | import androidx.work.WorkerParameters
21 | import com.optimizely.ab.android.shared.Client
22 | import com.optimizely.ab.android.shared.EventHandlerUtils
23 | import com.optimizely.ab.android.shared.OptlyStorage
24 | import org.slf4j.LoggerFactory
25 |
26 | class ODPEventWorker(context: Context, workerParams: WorkerParameters) :
27 | Worker(context, workerParams) {
28 |
29 | @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
30 | public var eventClient = ODPEventClient(
31 | Client(OptlyStorage(context), LoggerFactory.getLogger(Client::class.java)),
32 | LoggerFactory.getLogger(ODPEventClient::class.java)
33 | )
34 |
35 | override fun doWork(): Result {
36 | val data = inputData
37 | val apiEndpoint = getApiEndpointFromInputData(data)
38 | val apiKey = getApiKeyFromInputData(data)
39 | val body = getEventBodyFromInputData(data)
40 |
41 | if (apiEndpoint == null || apiKey == null || body == null) {
42 | // malformed request (not retried).
43 | return Result.failure()
44 | }
45 |
46 | val status = eventClient.dispatch(apiKey, apiEndpoint, body) ?: false
47 |
48 | // NOTE: retries on failure is taken care in ODPEventClient
49 | return if (status) Result.success() else Result.failure()
50 | }
51 |
52 | @VisibleForTesting
53 | fun getEventBodyFromInputData(inputData: Data): String? {
54 | // check non-compressed data first
55 | val body = inputData.getString(KEY_ODP_EVENT_BODY)
56 | if (body != null) return body
57 |
58 | // check if data compressed
59 | val compressed = inputData.getString(KEY_ODP_EVENT_BODY_COMPRESSED) ?: return null
60 | return try {
61 | EventHandlerUtils.decompress(compressed)
62 | } catch (e: Exception) {
63 | null
64 | }
65 | }
66 |
67 | @VisibleForTesting
68 | fun getApiEndpointFromInputData(data: Data): String? {
69 | return data.getString(KEY_ODP_EVENT_APIENDPOINT)
70 | }
71 |
72 | @VisibleForTesting
73 | fun getApiKeyFromInputData(data: Data): String? {
74 | return data.getString(KEY_ODP_EVENT_APIKEY)
75 | }
76 |
77 | companion object {
78 | const val workerId = "ODPEventWorker"
79 |
80 | const val KEY_ODP_EVENT_APIENDPOINT = "apiEndpoint"
81 | const val KEY_ODP_EVENT_APIKEY = "apiKey"
82 | const val KEY_ODP_EVENT_BODY = "body"
83 | const val KEY_ODP_EVENT_BODY_COMPRESSED = "bodyCompressed"
84 |
85 | fun getData(apiKey: String, apiEndpoint: String, payload: String): Data {
86 | // androidx.work.Data throws IllegalStateException if total data length is more than MAX_DATA_BYTES
87 | // compress larger body and decompress it before dispatching. The compress rate is very high because of repeated data (20KB -> 1KB, 45KB -> 1.5KB).
88 | val maxSizeBeforeCompress = Data.MAX_DATA_BYTES - 1000 // 1000 reserved for other meta data
89 |
90 | val builder = Data.Builder()
91 | .putString(KEY_ODP_EVENT_APIENDPOINT, apiEndpoint)
92 | .putString(KEY_ODP_EVENT_APIKEY, apiKey)
93 |
94 | if (payload.length < maxSizeBeforeCompress) {
95 | builder.putString(KEY_ODP_EVENT_BODY, payload)
96 | } else {
97 | val compressed: String = EventHandlerUtils.compress(payload)
98 | builder.putString(KEY_ODP_EVENT_BODY_COMPRESSED, compressed)
99 | }
100 |
101 | return builder.build()
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/odp/src/main/java/com/optimizely/ab/android/odp/VuidManager.kt:
--------------------------------------------------------------------------------
1 | // Copyright 2022, Optimizely, Inc. and contributors
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // https://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package com.optimizely.ab.android.odp
16 |
17 | import android.content.Context
18 | import androidx.annotation.VisibleForTesting
19 | import com.optimizely.ab.android.shared.OptlyStorage
20 | import java.util.UUID
21 |
22 | class VuidManager private constructor() {
23 | var vuid = ""
24 | private val keyForVuid = "vuid" // stored in the private "optly" storage
25 |
26 | companion object {
27 | @Volatile
28 | private var INSTANCE: VuidManager? = null
29 |
30 | @Synchronized
31 | fun getInstance(): VuidManager = INSTANCE ?: VuidManager().also { INSTANCE = it }
32 |
33 | fun isVuid(visitorId: String): Boolean {
34 | return visitorId.startsWith("vuid_", ignoreCase = true)
35 | }
36 |
37 | @VisibleForTesting
38 | fun removeSharedForTesting() {
39 | INSTANCE = null
40 | }
41 | }
42 |
43 | @Synchronized
44 | fun configure(enableVuid: Boolean, context: Context) {
45 | if (!enableVuid) {
46 | removeVuid(context)
47 | this.vuid = ""
48 | } else {
49 | this.vuid = load(context)
50 | }
51 | }
52 |
53 | @VisibleForTesting
54 | fun makeVuid(): String {
55 | val maxLength = 32 // required by ODP server
56 |
57 | // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details.
58 | val vuidFull = "vuid_" + UUID.randomUUID().toString().replace("-", "").lowercase()
59 | return if (vuidFull.length <= maxLength) vuidFull else vuidFull.substring(0, maxLength)
60 | }
61 |
62 | @VisibleForTesting
63 | fun load(context: Context): String {
64 | val storage = OptlyStorage(context)
65 | val oldVuid = storage.getString(keyForVuid, null)
66 | if (oldVuid != null) {
67 | return oldVuid
68 | }
69 |
70 | val vuid = makeVuid()
71 | save(context, vuid)
72 | return vuid
73 | }
74 |
75 | @VisibleForTesting
76 | fun save(context: Context, vuid: String) {
77 | val storage = OptlyStorage(context)
78 | storage.saveString(keyForVuid, vuid)
79 | }
80 |
81 | fun removeVuid(context: Context) {
82 | val storage = OptlyStorage(context)
83 | storage.remove(keyForVuid)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/proguard-rules.txt:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/jdeffibaugh/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | ## Below are the suggested rules from the developer documentation:
20 | ## https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=android&platform=mobile#installation
21 |
22 | # Optimizely
23 | -keep class com.optimizely.ab.config.**
24 | -keepclassmembers class com.optimizely.ab.config.** {
25 | *;
26 | }
27 | # Keep Payload classes that get sent to the ODP server
28 | -keep class com.optimizely.ab.odp.ODPEvent { *; }
29 | # ODP event uses this. R8 complains about it.
30 | -dontwarn java.beans.Transient
31 |
32 | # Keep Payload classes that get sent to Optimizely's backend
33 | -keep class com.optimizely.ab.event.internal.payload.** { *; }
34 |
35 | # Keep these for logging purposes
36 | -keep class com.optimizely.ab.bucketing.DecisionService { *; }
37 | -keep class com.optimizely.ab.Optimizely { *; }
38 |
39 | # Safely ignore warnings about other libraries since we are using Gson
40 | -dontwarn com.fasterxml.jackson.**
41 | -dontwarn org.json.**
42 |
43 | # Annotations
44 | -dontwarn javax.annotation.**
45 |
46 | # Findbugs
47 | -dontwarn edu.umd.cs.findbugs.annotations.SuppressFBWarnings
48 |
49 | # slf4j
50 | -dontwarn org.slf4j.**
51 | -keep class org.slf4j.** {*;}
52 |
53 | # Android Logger
54 | -keep class com.noveogroup.android.log.** { *; }
55 |
56 | -optimizations !class/unboxing/enum
57 |
58 | -dontwarn com.google.gson.**
59 | -dontwarn com.optimizely.ab.config.parser.**
60 |
61 | # Gson (https://github.com/google/gson/blob/master/examples/android-proguard-example/proguard.cfg)
62 | ##---------------Begin: proguard configuration for Gson ----------
63 | # Gson uses generic type information stored in a class file when working with fields. Proguard
64 | # removes such information by default, so configure it to keep all of it.
65 | -keepattributes Signature
66 |
67 | # For using GSON @Expose annotation
68 | -keepattributes *Annotation*
69 |
70 | # Gson specific classes
71 | -dontwarn sun.misc.**
72 |
73 | # Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
74 | # JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
75 | -keep class * extends com.google.gson.TypeAdapter
76 | -keep class * implements com.google.gson.TypeAdapterFactory
77 | -keep class * implements com.google.gson.JsonSerializer
78 | -keep class * implements com.google.gson.JsonDeserializer
79 |
80 | # Prevent R8 from leaving Data object members always null
81 | -keepclassmembers,allowobfuscation class * {
82 | @com.google.gson.annotations.SerializedName ;
83 | }
84 |
85 | # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher.
86 | -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken
87 | -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken
88 |
89 | # Retain the name used by the Java SDK to determine whether Gson is usable as a config parser.
90 | -keepnames class com.google.gson.Gson
91 | ##---------------End: proguard configuration for Gson ----------
92 |
93 |
--------------------------------------------------------------------------------
/scripts/android-wait-for-emulator.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Originally written by Ralf Kistner , but placed in the public domain
4 |
5 | set +e
6 |
7 | bootanim=""
8 | failcounter=0
9 | #timeout_in_sec=360 # 6 minutes
10 | timeout_in_sec=900 # 15 minutes
11 | echo "Waiting for emulator to start"
12 | until [[ "$bootanim" =~ "stopped" ]]; do
13 | bootanim=`adb -e shell getprop init.svc.bootanim 2>&1 &`
14 | #echo bootanim=\`$bootanim\`
15 | if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline"
16 | || "$bootanim" =~ "running" || "$bootanim" =~ "error: no emulators found" ]]; then
17 | let "failcounter += 5"
18 | echo -n "."
19 | if [[ $failcounter -gt timeout_in_sec ]]; then
20 | echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator"
21 | exit 1
22 | fi
23 | else
24 | if [[ ! "$bootanim" =~ "stopped" ]]; then
25 | echo "unexpected behavior from (adb -e shell getprop init.svc.bootanim): $bootanim"
26 | exit 1
27 | fi
28 | fi
29 | sleep 5
30 | done
31 |
32 | echo "Emulator is ready (took $failcounter seconds)"
33 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':event-handler', ':android-sdk', ':shared', ':test-app', ':user-profile', ':datafile-handler', ':odp'
2 |
--------------------------------------------------------------------------------
/shared/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/shared/build.gradle:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | apply plugin: 'com.android.library'
18 | apply plugin: 'kotlin-android'
19 |
20 | android {
21 | compileSdkVersion compile_sdk_version
22 | buildToolsVersion build_tools_version
23 |
24 | defaultConfig {
25 | minSdkVersion min_sdk_version
26 | targetSdkVersion target_sdk_version
27 | versionCode 1
28 | versionName "1.0"
29 | multiDexEnabled true
30 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
31 | }
32 | testOptions {
33 | unitTests.returnDefaultValues = true
34 | }
35 | buildTypes {
36 | release {
37 | minifyEnabled false
38 | }
39 | debug {
40 | testCoverageEnabled true
41 | }
42 | }
43 | compileOptions {
44 | sourceCompatibility JavaVersion.VERSION_17
45 | targetCompatibility JavaVersion.VERSION_17
46 | }
47 | buildToolsVersion build_tools_version
48 | }
49 |
50 | dependencies {
51 | api ("com.optimizely.ab:core-api:$java_core_ver") {
52 | exclude group: 'com.google.code.findbugs'
53 | }
54 |
55 | implementation "androidx.annotation:annotation:$annotations_ver"
56 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
57 | implementation "androidx.work:work-runtime:$work_runtime"
58 | // the default json parser for java-sdk core-api (app can exclude it to replace with other parsers)
59 | implementation "com.google.code.gson:gson:$gson_ver"
60 | // Base64
61 | implementation "commons-codec:commons-codec:1.15"
62 |
63 | compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
64 |
65 | testImplementation "junit:junit:$junit_ver"
66 | testImplementation "org.mockito:mockito-core:$mockito_ver"
67 | testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
68 |
69 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
70 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
71 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
72 | // Set this dependency to use JUnit 4 rules
73 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
74 | androidTestImplementation "androidx.test:core:$androidx_test_core"
75 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
76 |
77 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
78 | androidTestImplementation "org.mockito:mockito-android:$mockito_ver"
79 | androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
80 | androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
81 | }
82 |
--------------------------------------------------------------------------------
/shared/src/androidTest/java/com/optimizely/ab/android/shared/CacheTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import android.content.Context;
20 | import androidx.test.platform.app.InstrumentationRegistry;
21 | import androidx.test.ext.junit.runners.AndroidJUnit4;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 | import org.mockito.Mockito;
27 | import org.slf4j.Logger;
28 |
29 | import java.io.FileNotFoundException;
30 | import java.io.FileOutputStream;
31 | import java.io.IOException;
32 |
33 | import static junit.framework.Assert.assertEquals;
34 | import static junit.framework.Assert.assertFalse;
35 | import static junit.framework.Assert.assertNull;
36 | import static junit.framework.Assert.assertTrue;
37 | import static org.mockito.Mockito.mock;
38 | import static org.mockito.Mockito.verify;
39 | import static org.mockito.Mockito.when;
40 |
41 | /**
42 | * Tests for {@link CacheTest}
43 | */
44 | @RunWith(AndroidJUnit4.class)
45 | public class CacheTest {
46 |
47 | private static final String FILENAME = "foo.txt";
48 |
49 | private Cache cache;
50 | private Logger logger;
51 |
52 | @Before
53 | public void setup() {
54 | Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
55 | logger = mock(Logger.class);
56 | cache = new Cache(context, logger);
57 | }
58 |
59 | @Test
60 | public void testSaveExistsLoadAndDelete() throws IOException {
61 | assertTrue(cache.save(FILENAME, "bar"));
62 | assertTrue(cache.exists(FILENAME));
63 | String data = cache.load(FILENAME);
64 | assertEquals("bar", data);
65 | assertTrue(cache.delete(FILENAME));
66 | }
67 |
68 | @Test
69 | public void testDeleteFail() {
70 | assertFalse(cache.delete(FILENAME));
71 | }
72 |
73 | @Test
74 | public void testExistsFalse() {
75 | assertFalse(cache.exists(FILENAME));
76 | }
77 |
78 | @Test
79 | public void testLoadFileNotFoundExceptionReturnsNull() throws FileNotFoundException {
80 | Context context = mock(Context.class);
81 | Cache cache = new Cache(context, logger);
82 | when(context.openFileInput(FILENAME)).thenThrow(new FileNotFoundException());
83 | assertNull(cache.load(FILENAME));
84 | verify(logger).warn("Unable to load file {}.", FILENAME);
85 | }
86 |
87 | @Test
88 | public void testSaveFail() throws IOException {
89 | Context context = mock(Context.class);
90 | Cache cache = new Cache(context, logger);
91 | FileOutputStream fileOutputStream = mock(FileOutputStream.class);
92 |
93 | String data = "{}";
94 | Mockito.doThrow(new IOException()).when(fileOutputStream).write(data.getBytes());
95 | when(context.openFileOutput(FILENAME, Context.MODE_PRIVATE)).thenReturn(fileOutputStream);
96 | assertFalse(cache.save(FILENAME, data));
97 | verify(logger).error("Error saving file {}.", FILENAME);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/shared/src/androidTest/java/com/optimizely/ab/android/shared/OptlyStorageTest.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import android.content.Context;
20 | import androidx.test.platform.app.InstrumentationRegistry;
21 | import androidx.test.ext.junit.runners.AndroidJUnit4;
22 |
23 | import org.junit.Before;
24 | import org.junit.Test;
25 | import org.junit.runner.RunWith;
26 |
27 | import static junit.framework.Assert.assertEquals;
28 |
29 |
30 | /**
31 | * Tests for {@link OptlyStorage}
32 | */
33 | @RunWith(AndroidJUnit4.class)
34 | public class OptlyStorageTest {
35 |
36 | private OptlyStorage optlyStorage;
37 |
38 | @Before
39 | public void setup() {
40 | Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
41 | optlyStorage = new OptlyStorage(context);
42 | }
43 |
44 | @Test
45 | public void saveAndGetLong() {
46 | optlyStorage.saveLong("foo", 1);
47 | assertEquals(1L, optlyStorage.getLong("foo", 0L));
48 | }
49 | }
50 |
51 |
--------------------------------------------------------------------------------
/shared/src/androidTest/java/com/optimizely/ab/android/shared/TestWorker.java:
--------------------------------------------------------------------------------
1 | package com.optimizely.ab.android.shared;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.work.Data;
7 | import androidx.work.Worker;
8 | import androidx.work.WorkerParameters;
9 |
10 | public class TestWorker extends Worker {
11 | public static final String workerId = "TestWorker";
12 |
13 | // static counter to trace periodic work execution
14 | public static int count = 0;
15 |
16 | public TestWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
17 | super(context, workerParams);
18 | }
19 |
20 | @Override
21 | public Result doWork() {
22 | count++;
23 |
24 | Data input = getInputData();
25 | if (input.size() == 0) {
26 | return Result.failure();
27 | } else {
28 | return Result.success(input);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/shared/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/CachedCounter.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import android.content.Context;
20 |
21 | import org.slf4j.Logger;
22 | import org.slf4j.LoggerFactory;
23 |
24 | public class CachedCounter implements CountingIdlingResourceInterface {
25 | private final Cache cache;
26 | private final Logger logger = LoggerFactory.getLogger("CachedCounter");
27 | private final Context context;
28 | private final static String fileName = "OptlyCachedCounterKey";
29 | public CachedCounter(Context context) {
30 | this.context = context;
31 | this.cache = new Cache(context, logger);
32 | if (!cache.exists(fileName)) {
33 | cache.save(fileName, "0");
34 | }
35 | }
36 |
37 | synchronized public void increment() {
38 | String value = cache.load(fileName);
39 | Integer val = Integer.valueOf(value);
40 | val += 1;
41 | cache.save(fileName, val.toString());
42 | }
43 |
44 | synchronized public void decrement() {
45 | String value = cache.load(fileName);
46 | Integer val = Integer.valueOf(value);
47 | if (val == 0) {
48 | return;
49 | }
50 | val -= 1;
51 | cache.save(fileName, val.toString());
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/CountingIdlingResourceInterface.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | /**
20 | * Interface for dealing with Espresso idling counter
21 | */
22 | public interface CountingIdlingResourceInterface {
23 | public void increment();
24 | public void decrement();
25 | }
26 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/CountingIdlingResourceManager.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import android.content.Context;
20 | import androidx.annotation.NonNull;
21 | import androidx.annotation.Nullable;
22 | import android.util.Pair;
23 |
24 | import java.util.LinkedList;
25 | import java.util.List;
26 |
27 | /**
28 | * Manages an Espresso like CountingIdlingResource
29 | */
30 | public class CountingIdlingResourceManager {
31 |
32 | @Nullable private static CountingIdlingResourceInterface countingIdlingResource;
33 | @NonNull private static List> eventList = new LinkedList<>();
34 |
35 | @Nullable public static CountingIdlingResourceInterface getIdlingResource(Context context) {
36 | if (countingIdlingResource == null) {
37 | countingIdlingResource = new CachedCounter(context);
38 | }
39 | return countingIdlingResource;
40 | }
41 |
42 | public static void setIdlingResource(@NonNull CountingIdlingResourceInterface countingIdlingResource) {
43 | CountingIdlingResourceManager.countingIdlingResource = countingIdlingResource;
44 | }
45 |
46 | public static void increment() {
47 | if (countingIdlingResource != null) {
48 | countingIdlingResource.increment();
49 | }
50 | }
51 |
52 | public static void decrement() {
53 | if (countingIdlingResource != null) {
54 | countingIdlingResource.decrement();
55 | }
56 | }
57 |
58 | public static void recordEvent(Pair event) {
59 | if (countingIdlingResource != null) {
60 | eventList.add(event);
61 | }
62 | }
63 |
64 | public static void clearEvents() {
65 | eventList.clear();
66 | }
67 |
68 | public static List> getEvents() {
69 | return eventList;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/EventHandlerUtils.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2022, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import androidx.annotation.NonNull;
20 |
21 | import org.apache.commons.codec.binary.Base64;
22 |
23 | import java.io.ByteArrayOutputStream;
24 | import java.io.IOException;
25 | import java.util.zip.Deflater;
26 | import java.util.zip.Inflater;
27 |
28 | public class EventHandlerUtils {
29 |
30 | private static final int BUFFER_SIZE = 32*1024;
31 |
32 | public static String compress(@NonNull String decompressed) throws IOException {
33 | byte[] data = decompressed.getBytes();
34 |
35 | final Deflater deflater = new Deflater();
36 | deflater.setInput(data);
37 |
38 | try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length)) {
39 | deflater.finish();
40 | final byte[] buffer = new byte[BUFFER_SIZE];
41 | while (!deflater.finished()) {
42 | final int count = deflater.deflate(buffer);
43 | outputStream.write(buffer, 0, count);
44 | }
45 |
46 | byte[] bytes = outputStream.toByteArray();
47 | // encoded to Base64 (instead of byte[] since WorkManager.Data size is unexpectedly expanded with byte[]).
48 | return encodeToBase64(bytes);
49 | }
50 | finally {
51 | deflater.end();
52 | }
53 | }
54 |
55 | public static String decompress(@NonNull String base64) throws Exception {
56 | byte[] data = decodeFromBase64(base64);
57 |
58 | final Inflater inflater = new Inflater();
59 | inflater.setInput(data);
60 |
61 | try (final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length)) {
62 | byte[] buffer = new byte[BUFFER_SIZE];
63 | while (!inflater.finished()) {
64 | final int count = inflater.inflate(buffer);
65 | outputStream.write(buffer, 0, count);
66 | }
67 |
68 | return outputStream.toString();
69 | }
70 | finally {
71 | inflater.end();
72 | }
73 | }
74 |
75 | static String encodeToBase64(byte[] bytes) {
76 | // - org.apache.commons.Base64 is used (instead of android.util.Base64) for unit testing
77 | // - encodeBase64() for backward compatibility (instead of encodeBase64String()).
78 | String base64 = "";
79 | if (bytes != null) {
80 | byte[] encoded = Base64.encodeBase64(bytes);
81 | base64 = new String(encoded);
82 | }
83 | return base64;
84 | }
85 |
86 | static byte[] decodeFromBase64(String base64) {
87 | return Base64.decodeBase64(base64.getBytes());
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/OptlyStorage.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import android.content.Context;
20 | import android.content.SharedPreferences;
21 | import androidx.annotation.NonNull;
22 |
23 | /**
24 | * Wrapper for {@link SharedPreferences}
25 | * @hide
26 | */
27 | public class OptlyStorage {
28 |
29 | private static final String PREFS_NAME = "optly";
30 |
31 | @NonNull private Context context;
32 |
33 | /**
34 | * Creates a new instance of {@link OptlyStorage}
35 | *
36 | * @param context any instance of {@link Context}
37 | * @hide
38 | */
39 | public OptlyStorage(@NonNull Context context) {
40 | this.context = context;
41 | }
42 |
43 | /**
44 | * Save a long value
45 | *
46 | * @param key a String key
47 | * @param value a long value
48 | * @hide
49 | */
50 | public void saveLong(String key, long value) {
51 | getWritablePrefs().putLong(key, value ).apply();
52 | }
53 |
54 | /**
55 | * Get a long value
56 | * @param key a String key
57 | * @param defaultValue the value to return if the key isn't stored
58 | * @return the long value
59 | * @hide
60 | */
61 | public long getLong(String key, long defaultValue) {
62 | return getReadablePrefs().getLong(key, defaultValue);
63 | }
64 |
65 | /**
66 | * Save a string value
67 | *
68 | * @param key a String key
69 | * @param value a string value
70 | * @hide
71 | */
72 | public void saveString(String key, String value) {
73 | getWritablePrefs().putString(key, value).apply();
74 | }
75 |
76 | /**
77 | * Get a string value
78 | * @param key a String key
79 | * @param defaultValue the value to return if the key isn't stored
80 | * @return the string value
81 | * @hide
82 | */
83 | public String getString(String key, String defaultValue) {
84 | return getReadablePrefs().getString(key, defaultValue);
85 | }
86 |
87 | private SharedPreferences.Editor getWritablePrefs() {
88 | return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit();
89 | }
90 |
91 | private SharedPreferences getReadablePrefs() {
92 | return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
93 | }
94 |
95 | public void remove(String key) {
96 | getWritablePrefs().remove(key).apply();
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/shared/src/main/java/com/optimizely/ab/android/shared/TLSSocketFactory.java:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2021, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | package com.optimizely.ab.android.shared;
18 |
19 | import java.io.IOException;
20 | import java.net.InetAddress;
21 | import java.net.Socket;
22 | import java.net.UnknownHostException;
23 | import java.security.KeyManagementException;
24 | import java.security.NoSuchAlgorithmException;
25 |
26 | import javax.net.ssl.SSLContext;
27 | import javax.net.ssl.SSLSocket;
28 | import javax.net.ssl.SSLSocketFactory;
29 |
30 | // Android uses TLS1.2 as a default for API 21+
31 | // This factory is to set TLS1.2 as default for older devices (datafile download and event uploading)
32 |
33 | public class TLSSocketFactory extends SSLSocketFactory {
34 | private SSLSocketFactory sslSocketFactory;
35 |
36 | public TLSSocketFactory() throws KeyManagementException, NoSuchAlgorithmException {
37 | SSLContext context = SSLContext.getInstance("TLS");
38 | context.init(null, null, null);
39 | sslSocketFactory = context.getSocketFactory();
40 | }
41 |
42 | @Override
43 | public String[] getDefaultCipherSuites() {
44 | return sslSocketFactory.getDefaultCipherSuites();
45 | }
46 |
47 | @Override
48 | public String[] getSupportedCipherSuites() {
49 | return sslSocketFactory.getSupportedCipherSuites();
50 | }
51 |
52 | @Override
53 | public Socket createSocket() throws IOException {
54 | return enableTLSOnSocket(sslSocketFactory.createSocket());
55 | }
56 |
57 | @Override
58 | public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
59 | return enableTLSOnSocket(sslSocketFactory.createSocket(s, host, port, autoClose));
60 | }
61 |
62 | @Override
63 | public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
64 | return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
65 | }
66 |
67 | @Override
68 | public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
69 | return enableTLSOnSocket(sslSocketFactory.createSocket(host, port, localHost, localPort));
70 | }
71 |
72 | @Override
73 | public Socket createSocket(InetAddress host, int port) throws IOException {
74 | return enableTLSOnSocket(sslSocketFactory.createSocket(host, port));
75 | }
76 |
77 | @Override
78 | public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
79 | return enableTLSOnSocket(sslSocketFactory.createSocket(address, port, localAddress, localPort));
80 | }
81 |
82 | private Socket enableTLSOnSocket(Socket socket) {
83 | if(socket instanceof SSLSocket) {
84 | ((SSLSocket)socket).setEnabledProtocols(new String[] {"TLSv1.2"});
85 | }
86 | return socket;
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/shared/src/test/java/com/optimizely/ab/android/shared/EventHandlerUtilsTest.java:
--------------------------------------------------------------------------------
1 | package com.optimizely.ab.android.shared;
2 |
3 | import static org.junit.Assert.assertEquals;
4 |
5 | import org.junit.Test;
6 |
7 | public class EventHandlerUtilsTest {
8 |
9 | @Test
10 | public void compressAndDecompress() throws Exception {
11 | String str = makeRandomString(1000);
12 |
13 | String compressed = EventHandlerUtils.compress(str);
14 | assert(compressed.length() < (str.length() * 0.5));
15 |
16 | String decompressed = EventHandlerUtils.decompress(compressed);
17 | assertEquals(str, decompressed);
18 | }
19 |
20 | @Test(timeout=30000)
21 | public void measureCompressionDelay() throws Exception {
22 | int maxEventSize = 100000; // 100KB (~100 attributes)
23 | int count = 3000;
24 |
25 | String body = EventHandlerUtilsTest.makeRandomString(maxEventSize);
26 |
27 | long start = System.currentTimeMillis();
28 | for (int i = 0; i < count; i++) {
29 | EventHandlerUtils.compress(body);
30 | }
31 | long end = System.currentTimeMillis();
32 | float delayCompress = ((float)(end - start))/count;
33 | System.out.println("Compression Delay: " + String.valueOf(delayCompress) + " millisecs");
34 | assert(delayCompress < 10); // less than 1ms for 100KB (set 10ms upperbound)
35 |
36 | start = System.currentTimeMillis();
37 | for (int i = 0; i < count; i++) {
38 | String compressed = EventHandlerUtils.compress(body);
39 | EventHandlerUtils.decompress(compressed);
40 | }
41 | end = System.currentTimeMillis();
42 | float delayDecompress = ((float)(end - start))/count - delayCompress;
43 | System.out.println("Decompression Delay: " + String.valueOf(delayDecompress) + " millisecs");
44 | assert(delayDecompress < 10); // less than 1ms for 100KB (set 10ms upperbound)
45 | }
46 |
47 | public static String makeRandomString(int maxSize) {
48 | StringBuilder builder = new StringBuilder();
49 |
50 | // for high compression rate, shift repeated string window.
51 | int window = 100;
52 | int shift = 3; // adjust (1...10) this for compression rate. smaller for higher rates.
53 |
54 | int start = 0;
55 | int end = start + window;
56 | int i = 0;
57 |
58 | int size = 0;
59 | while (true) {
60 | String str = String.valueOf(i);
61 | size += str.length();
62 | if (size > maxSize) {
63 | break;
64 | }
65 | builder.append(str);
66 |
67 | i++;
68 | if (i > end) {
69 | start = start + shift;
70 | end = start + window;
71 | i = start;
72 | }
73 | }
74 |
75 | return builder.toString();
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/test-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | src/main/res/values/git_ignored_strings.xml
3 |
--------------------------------------------------------------------------------
/test-app/README.md:
--------------------------------------------------------------------------------
1 | # Optimizely X Android SDK Demo App
2 |
3 | This module houses the demo app used to demonstrate how to get started with the Android SDK. The app
4 | is also used to run integration tests with the Android Espresso framework.
5 |
6 | 
7 |
8 | ## Experiments Run
9 |
10 | We run the following experiment:
11 | - Background change in second activity, which is loaded after the splash screen.
12 |
13 | ## How it works
14 |
15 | The SDK is implemented in the following way:
16 | - The splash screen initializes the Optimizely manager asynchronously. This starts the datafile
17 | fetch.
18 | - Once the datafile is fetched and the Optimizely manager is started, we grab the `optimizelyClient`
19 | from the manager and use it to activate the experiment named `background_experiment`. This buckets the user and sends
20 | an impression event.
21 | - We then use the bucketed variation to determine which activity to show. `VariationAActivity` for
22 | `variation_a`, `VariationBActivity` for `variation_b`, or `ActivationErrorActivity` for the control.
23 | - Each of the variation activities includes a `Test Conversion` button.
24 | - Clicking on that button will call `optimizelyClient.track()` and send a conversion event for the
25 | event named `sample_conversion`.
26 | - Then the application will navigate to the conversion page to confirm that a conversion event has
27 | been sent.
28 |
29 | ## Running the test app
30 |
31 | Run `./gradlew test-app:connectedAndroidTest` to run the Espresso tests.
32 |
33 |
--------------------------------------------------------------------------------
/test-app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'kotlin-android'
3 |
4 | android {
5 | compileSdkVersion compile_sdk_version
6 | buildToolsVersion build_tools_version
7 |
8 |
9 | defaultConfig {
10 | minSdkVersion 21
11 | targetSdkVersion target_sdk_version
12 | versionCode 1
13 | versionName "1.0.2"
14 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
15 | }
16 | testOptions {
17 | unitTests.returnDefaultValues = true
18 | }
19 | buildTypes {
20 | debug {
21 | // enable proguard for debug mode (keep both of these to detect issues while testing)
22 | minifyEnabled true
23 | debuggable false
24 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'test-app-proguard-rules.pro'
25 | }
26 | release {
27 | minifyEnabled true
28 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'test-app-proguard-rules.pro'
29 | signingConfig signingConfigs.debug
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility JavaVersion.VERSION_17
34 | targetCompatibility JavaVersion.VERSION_17
35 | }
36 | packagingOptions {
37 | resources {
38 | excludes += ['META-INF/LICENSE.txt', 'META-INF/NOTICE.txt', 'META-INF/LICENSE', 'META-INF/NOTICE']
39 | }
40 | }
41 |
42 | lint {
43 | abortOnError false
44 | }
45 | }
46 |
47 | dependencies {
48 | // Includes the Optimizely Feature Experimentation, Optimizely Full Stack (legacy), and Optimizely Rollouts Java SDK, event handler, and user profile
49 | //implementation "com.optimizely.ab:android-sdk:1.0.0"
50 | implementation (project(':android-sdk')) {
51 | exclude group: 'com.google.code.gson', module:'gson'
52 | }
53 |
54 | // EXAMPLE - replace gson with jackson-databind json parser
55 | implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
56 |
57 | // SLF4J logger
58 | // https://github.com/noveogroup/android-logger (resources/android-logger.properties)
59 | implementation "com.noveogroup.android:android-logger:$android_logger_ver"
60 | // https://mvnrepository.com/artifact/org.slf4j/slf4j-android
61 | //implementation "org.slf4j:slf4j-android:1.7.25"
62 |
63 | implementation "androidx.appcompat:appcompat:1.2.0"
64 | implementation "com.google.android.material:material:1.2.1"
65 |
66 | // required by API-16
67 | implementation "com.google.code.gson:gson:$gson_ver"
68 | implementation 'com.google.code.findbugs:annotations:3.0.0'
69 |
70 | testImplementation "junit:junit:$junit_ver"
71 | testImplementation "org.mockito:mockito-core:$mockito_ver"
72 | testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
73 | // testImplementation 'com.optimizely.ab:android-sdk:1.0.0'
74 | testImplementation project(':android-sdk')
75 |
76 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
77 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
78 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
79 | // Set this dependency to use JUnit 4 rules
80 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
81 | androidTestImplementation "androidx.test:core:$androidx_test_core"
82 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
83 |
84 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
85 | androidTestImplementation "com.crittercism.dexmaker:dexmaker:$dexmaker_ver"
86 | androidTestImplementation "com.crittercism.dexmaker:dexmaker-dx:$dexmaker_ver"
87 | androidTestImplementation "com.crittercism.dexmaker:dexmaker-mockito:$dexmaker_ver"
88 | // androidTestImplementation 'com.optimizely.ab:android-sdk:1.0.0'
89 | androidTestImplementation project(':android-sdk')
90 | androidTestImplementation project(path: ':shared')
91 | androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
92 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
93 | }
94 |
95 |
--------------------------------------------------------------------------------
/test-app/images/demo-app-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/test-app/images/demo-app-flow.png
--------------------------------------------------------------------------------
/test-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
37 |
40 |
41 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/ActivationErrorActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.os.Bundle
19 | import androidx.appcompat.app.AppCompatActivity
20 | import android.view.View
21 | import android.widget.Button
22 |
23 | class ActivationErrorActivity : AppCompatActivity() {
24 | override fun onCreate(savedInstanceState: Bundle?) {
25 | super.onCreate(savedInstanceState)
26 | setContentView(R.layout.activity_activation_error)
27 | val btnConversion = findViewById(R.id.btn_conversion_error_back) as Button
28 | btnConversion.setOnClickListener { onBackPressed() }
29 | }
30 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.content.Context
19 | import android.content.Intent
20 | import android.os.Bundle
21 | import android.os.PersistableBundle
22 | import androidx.appcompat.app.AppCompatActivity
23 | import android.util.AttributeSet
24 | import android.view.View
25 |
26 | open class BaseActivity : AppCompatActivity() {
27 | private var showCoupon = false
28 | open fun setShowCoupon(v: Boolean?) {
29 | v?.let {
30 | showCoupon = it
31 | }
32 | }
33 | open fun getShowCoupon(): Boolean {
34 | return showCoupon
35 | }
36 |
37 | override fun onCreate(savedInstanceState: Bundle?) {
38 | super.onCreate(savedInstanceState)
39 | showCoupon = intent.getBooleanExtra("show_coupon", false)
40 |
41 | val application = applicationContext as MyApplication
42 | application?.currentActivity = this
43 | if (showCoupon) {
44 | intent = Intent(this, CouponActivity::class.java)
45 | startActivity(intent)
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/ConversionFragment.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.app.Fragment
19 | import android.content.Intent
20 | import android.os.Bundle
21 | import android.view.LayoutInflater
22 | import android.view.View
23 | import android.view.ViewGroup
24 | import android.widget.Button
25 | import com.optimizely.ab.android.shared.CountingIdlingResourceManager
26 |
27 | class ConversionFragment : Fragment() {
28 | private var conversionButton: Button? = null
29 | override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
30 | return inflater.inflate(R.layout.fragment_conversion, container, false)
31 | }
32 |
33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
34 | conversionButton = view.findViewById(R.id.btn_variation_conversion) as Button
35 | val myApplication = activity.application as MyApplication
36 | val optimizelyManager = myApplication.optimizelyManager
37 | conversionButton?.setOnClickListener {
38 | val userId = myApplication.anonUserId
39 |
40 | // This tracks a conversion event for the event named `sample_conversion`
41 | val optimizely = optimizelyManager?.optimizely
42 | optimizely?.track("sample_conversion", userId)
43 | optimizely?.track("my_conversion", userId)
44 |
45 | // Utility method for verifying event dispatches in our automated tests
46 | CountingIdlingResourceManager.increment() // increment for conversion event
47 | val intent = Intent(myApplication.baseContext, EventConfirmationActivity::class.java)
48 | startActivity(intent)
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/CouponActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.content.Context
19 | import android.content.res.ColorStateList
20 | import android.graphics.Color
21 | import android.os.Bundle
22 | import androidx.appcompat.app.AppCompatActivity
23 | import android.util.AttributeSet
24 | import android.view.View
25 | import android.widget.TextView
26 |
27 | class CouponActivity : BaseActivity() {
28 | override fun setShowCoupon(v: Boolean?) {
29 | super.setShowCoupon(v)
30 | if (!getShowCoupon()) {
31 | finish()
32 | }
33 | }
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | setContentView(R.layout.coupon)
37 |
38 | val button = findViewById(R.id.redeem)
39 |
40 | val txt = findViewById(R.id.message) as TextView
41 |
42 | val app = application as MyApplication
43 |
44 | val userId = app.anonUserId ?: "something"
45 | val attributes = app.attributes ?: HashMap()
46 |
47 | val message = app.optimizelyManager?.optimizely?.getFeatureVariableString("show_coupon", "message", userId, attributes)
48 | val discount = app.optimizelyManager?.optimizely?.getFeatureVariableInteger("show_coupon", "discount", userId, attributes)
49 |
50 | txt.text = "$message $discount%"
51 |
52 | when (app.optimizelyManager?.optimizely?.getFeatureVariableString("show_coupon", "text_color", userId, attributes)) {
53 | "yellow" -> txt.setTextColor(Color.YELLOW)
54 | }
55 |
56 | button.setOnClickListener() {
57 | finish()
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/EventConfirmationActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.os.Bundle
19 | import androidx.appcompat.app.AppCompatActivity
20 |
21 | class EventConfirmationActivity : AppCompatActivity() {
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_event_confirmation)
25 | }
26 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/MyApplication.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2021, 2023 Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.app.Application
19 | import android.content.Context
20 | import android.os.Build
21 | import com.optimizely.ab.android.sdk.OptimizelyManager
22 | import java.util.*
23 | import java.util.concurrent.TimeUnit
24 |
25 | class MyApplication : Application() {
26 | var optimizelyManager: OptimizelyManager? = null
27 | private set
28 |
29 | val attributes: Map
30 | get() {
31 | val attributes: MutableMap = HashMap()
32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
33 | attributes["locale"] = resources.configuration.locales[0].toString()
34 | } else {
35 | attributes["locale"] = resources.configuration.locale.toString()
36 | }
37 | val abbr = getAbbriviation(getLocality(location))
38 |
39 | attributes["location"] = abbr
40 | attributes["semanticVersioning"] = "2.1.0"
41 |
42 | return attributes
43 | }
44 |
45 | val anonUserId: String
46 | get() {
47 | // this is a convenience method that creates and persists an anonymous user id,
48 | // which we need to pass into the activate and track calls
49 | val sharedPreferences = getSharedPreferences("user", Context.MODE_PRIVATE)
50 | var id = sharedPreferences.getString("userId", null)
51 | if (id == null) {
52 | id = UUID.randomUUID().toString()
53 |
54 | // comment this out to get a brand new user id every time this function is called.
55 | // useful for incrementing results page count for QA purposes
56 | sharedPreferences.edit().putString("userId", id).apply()
57 | }
58 | return id
59 | }
60 |
61 | private val userAge: Int
62 | private get() = 65
63 |
64 | override fun onCreate() {
65 | super.onCreate()
66 |
67 | // This app is built against a real Optimizely project with real experiments set. Automated
68 | // espresso tests are run against this project id. Changing it will make the Optimizely
69 | // must match the project id of the compiled in Optimizely data file in rest/raw/data_file.json.
70 | optimizelyManager = OptimizelyManager.builder()
71 | .withEventDispatchInterval(60L, TimeUnit.SECONDS)
72 | .withDatafileDownloadInterval(15, TimeUnit.MINUTES)
73 | .withSDKKey("FCnSegiEkRry9rhVMroit4")
74 | .build(applicationContext)
75 | }
76 |
77 | var currentActivity: BaseActivity? = null
78 |
79 | private fun getAbbriviation(locality: Any): String {
80 | return "NY"
81 | }
82 |
83 | private fun getLocality(location: Any?): Any {
84 | return "NY"
85 | }
86 |
87 | private val location: Any?
88 | private get() = null
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/NotificationService.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2017, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.app.IntentService
19 | import android.content.Intent
20 | import androidx.core.app.NotificationCompat
21 | import com.optimizely.ab.android.shared.CountingIdlingResourceManager
22 |
23 | class NotificationService : IntentService("NotificationService") {
24 | override fun onHandleIntent(intent: Intent?) {
25 | if (intent != null) {
26 | showNotification()
27 |
28 | // Get Optimizely from the Intent that started this Service
29 | val myApplication = application as MyApplication
30 | val optimizelyManager = myApplication.optimizelyManager
31 | val optimizely = optimizelyManager!!.optimizely
32 | CountingIdlingResourceManager.increment()
33 | optimizely.track("experiment_2", myApplication.anonUserId)
34 | }
35 | }
36 |
37 | private fun showNotification() {
38 | val mBuilder = NotificationCompat.Builder(this)
39 | .setSmallIcon(android.R.drawable.ic_notification_clear_all)
40 | .setContentTitle(getString(R.string.notification_service_notification_title))
41 | .setContentText(getString(R.string.notification_service_notification_text))
42 | // @TODO(mng): Add notification back when we add more complex scenario
43 | // Creates an explicit intent for an Activity in your app
44 | // Intent resultIntent = new Intent(this, MainActivity.class);
45 | //
46 | // // The stack builder object will contain an artificial back stack for the
47 | // // started Activity.
48 | // // This ensures that navigating backward from the Activity leads out of
49 | // // your application to the Home screen.
50 | // TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
51 | // // Adds the back stack for the Intent (but not the Intent itself)
52 | // stackBuilder.addParentStack(SecondaryActivity.class);
53 | // // Adds the Intent that starts the Activity to the top of the stack
54 | // stackBuilder.addNextIntent(resultIntent);
55 | // PendingIntent resultPendingIntent =
56 | // stackBuilder.getPendingIntent(
57 | // 0,
58 | // PendingIntent.FLAG_UPDATE_CURRENT
59 | // );
60 | // mBuilder.setContentIntent(resultPendingIntent);
61 | // NotificationManager mNotificationManager =
62 | // (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
63 | // // mId allows you to update the notification later on.
64 | // mNotificationManager.notify(0, mBuilder.build());
65 | }
66 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/VariationAActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.content.Context
19 | import android.content.Intent
20 | import android.os.Bundle
21 | import android.util.AttributeSet
22 | import android.view.View
23 |
24 | class VariationAActivity : BaseActivity() {
25 | override fun onCreate(savedInstanceState: Bundle?) {
26 | super.onCreate(savedInstanceState)
27 | setContentView(R.layout.activity_variation_a)
28 | }
29 |
30 | override fun setShowCoupon(v: Boolean?) {
31 | super.setShowCoupon(v)
32 | if (getShowCoupon()) {
33 | val app = applicationContext as MyApplication
34 | val intent = Intent(this, CouponActivity::class.java)
35 | startActivity(intent)
36 | }
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/test-app/src/main/java/com/optimizely/ab/android/test_app/VariationBActivity.kt:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2017-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | */
16 | package com.optimizely.ab.android.test_app
17 |
18 | import android.content.Intent
19 | import android.os.Bundle
20 |
21 | class VariationBActivity : BaseActivity() {
22 | override fun onCreate(savedInstanceState: Bundle?) {
23 | super.onCreate(savedInstanceState)
24 | setContentView(R.layout.activity_variation_b)
25 | }
26 |
27 | override fun setShowCoupon(v: Boolean?) {
28 | super.setShowCoupon(v)
29 | if (getShowCoupon()) {
30 | val app = applicationContext as MyApplication
31 | val intent = Intent(this, CouponActivity::class.java)
32 | startActivity(intent)
33 | }
34 | }
35 |
36 | }
--------------------------------------------------------------------------------
/test-app/src/main/res/drawable/ic_background_varia.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/test-app/src/main/res/drawable/ic_background_varib_marina.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/test-app/src/main/res/drawable/ic_optimizely_logo.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/activity_activation_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
30 |
39 |
49 |
62 |
63 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/activity_event_confirmation.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
30 |
41 |
42 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/activity_splash_screen.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
31 |
39 |
40 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/activity_variation_a.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
31 |
40 |
53 |
54 |
62 |
63 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/activity_variation_b.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
31 |
40 |
53 |
54 |
62 |
63 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/coupon.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
19 |
20 |
27 |
28 |
33 |
--------------------------------------------------------------------------------
/test-app/src/main/res/layout/fragment_conversion.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
25 |
26 |
35 |
36 |
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/test-app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/test-app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/test-app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/optimizely/android-sdk/841a5d236d822e53803e4064665663b221057b64/test-app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/test-app/src/main/res/raw/datafile.json:
--------------------------------------------------------------------------------
1 | {
2 | "groups": [],
3 | "variables": [],
4 | "experiments": [
5 | {
6 | "status": "Running",
7 | "key": "background_experiment",
8 | "layerId": "8143622708",
9 | "trafficAllocation": [
10 | {
11 | "entityId": "8146590584",
12 | "endOfRange": 5000
13 | },
14 | {
15 | "entityId": "8134412703",
16 | "endOfRange": 10000
17 | }
18 | ],
19 | "audienceIds": [],
20 | "variations": [
21 | {
22 | "variables": [],
23 | "id": "8146590584",
24 | "key": "variation_a"
25 | },
26 | {
27 | "variables": [],
28 | "id": "8134412703",
29 | "key": "variation_b"
30 | }
31 | ],
32 | "forcedVariations": {
33 | "test_user": "variation_a"
34 | },
35 | "id": "8126664113"
36 | }
37 | ],
38 | "audiences": [],
39 | "anonymizeIP": false,
40 | "accountId": "6384711706",
41 | "projectId": "8136462271",
42 | "version": "3",
43 | "attributes": [],
44 | "events": [
45 | {
46 | "experimentIds": [
47 | "8126664113"
48 | ],
49 | "id": "8128595839",
50 | "key": "sample_conversion"
51 | }
52 | ],
53 | "revision": "5"
54 | }
--------------------------------------------------------------------------------
/test-app/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #003952
4 | #1976D2
5 | #ff5500
6 | #FFFFFF
7 |
8 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 |
6 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Optimizely Demo App
3 | Next Activity
4 | Start Next Activity!
5 | Start Second Activity!
6 | Optimizely X Test App
7 | Optimizely X
8 | Optimizely Test App
9 | Text View 1
10 | Inside of a Fragment
11 | We\'re Inside of a Fragment
12 | We are Inside of a Fragment
13 | Show Toast
14 | Optimizely X Android
15 | Works in services!
16 |
17 |
--------------------------------------------------------------------------------
/test-app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/test-app/src/main/resources/android-logger.properties:
--------------------------------------------------------------------------------
1 | #****************************************************************************
2 | # Copyright 2016, Optimizely, Inc. and contributors *
3 | # *
4 | # Licensed under the Apache License, Version 2.0 (the "License"); *
5 | # you may not use this file except in compliance with the License. *
6 | # You may obtain a copy of the License at *
7 | # *
8 | # http://www.apache.org/licenses/LICENSE-2.0 *
9 | # *
10 | # Unless required by applicable law or agreed to in writing, software *
11 | # distributed under the License is distributed on an "AS IS" BASIS, *
12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | # See the License for the specific language governing permissions and *
14 | # limitations under the License. *
15 | #**************************************************************************/
16 |
17 | # Android Logger configuration example
18 |
19 | # https://github.com/noveogroup/android-logger
20 | # Valid logging levels are: VERBOSE, DEBUG, INFO, WARN, ERROR, ASSERT
21 |
22 | # Default configuration
23 | root=DEBUG:Optly
24 |
25 | # Core
26 | logger.com.optimizely.ab.Optimizely=DEBUG:Optly.core
27 | logger.com.optimizely.ab.annotations=DEBUG:Optly.annotations
28 | logger.com.optimizely.ab.bucketing=DEBUG:Optly.bucketing
29 | logger.com.optimizely.ab.config=DEBUG:Optly.config
30 | logger.com.optimizely.ab.error=DEBUG:Optly.error
31 | logger.com.optimizely.ab.event=DEBUG:Optly.event
32 | logger.com.optimizely.ab.internal=DEBUG:Optly.internal
33 |
34 | # Android
35 | logger.com.optimizely.ab.android.sdk=DEBUG:Optly.androidSdk
36 | logger.com.optimizely.ab.android.datafile_handler=DEBUG:Optly.datafileHandler
37 | logger.com.optimizely.ab.android.event_handler=DEBUG:Optly.eventHandler
38 | logger.com.optimizely.ab.android.shared=DEBUG:Optly.shared
39 | logger.com.optimizely.ab.android.user-profile=DEBUG:Optly.userProfile
40 |
41 | # Disable most SDK logs by commenting all other lines and uncommenting below
42 | #logger.com.optimizely.ab=ASSERT:Optly
43 |
--------------------------------------------------------------------------------
/test-app/test-app-proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /Users/jdeffibaugh/Library/Android/sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # when gson is replaced with jackson-databind json parser
11 | -keep class com.fasterxml.jackson.** {*;}
12 |
13 | #-printconfiguration proguard-merged-config.txt
14 |
--------------------------------------------------------------------------------
/user-profile/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/user-profile/build.gradle:
--------------------------------------------------------------------------------
1 | /****************************************************************************
2 | * Copyright 2016-2020, Optimizely, Inc. and contributors *
3 | * *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); *
5 | * you may not use this file except in compliance with the License. *
6 | * You may obtain a copy of the License at *
7 | * *
8 | * http://www.apache.org/licenses/LICENSE-2.0 *
9 | * *
10 | * Unless required by applicable law or agreed to in writing, software *
11 | * distributed under the License is distributed on an "AS IS" BASIS, *
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. *
13 | * See the License for the specific language governing permissions and *
14 | * limitations under the License. *
15 | ***************************************************************************/
16 |
17 | apply plugin: 'com.android.library'
18 | apply plugin: 'kotlin-android'
19 |
20 | android {
21 | compileSdkVersion compile_sdk_version
22 | buildToolsVersion build_tools_version
23 |
24 | defaultConfig {
25 | minSdkVersion min_sdk_version
26 | targetSdkVersion target_sdk_version
27 | multiDexEnabled true
28 | testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
29 | }
30 | testOptions {
31 | unitTests.returnDefaultValues = true
32 | }
33 | buildTypes {
34 | release {
35 | minifyEnabled false
36 | }
37 | debug {
38 | testCoverageEnabled true
39 | }
40 | }
41 | compileOptions {
42 | sourceCompatibility JavaVersion.VERSION_17
43 | targetCompatibility JavaVersion.VERSION_17
44 | }
45 | }
46 |
47 | dependencies {
48 | api project(':shared')
49 |
50 | implementation "androidx.annotation:annotation:$annotations_ver"
51 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
52 |
53 | compileOnly "com.noveogroup.android:android-logger:$android_logger_ver"
54 |
55 | testImplementation "junit:junit:$junit_ver"
56 | testImplementation "org.mockito:mockito-core:$mockito_ver"
57 | testImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
58 |
59 | androidTestImplementation "androidx.work:work-testing:$work_runtime"
60 | androidTestImplementation "androidx.test.ext:junit:$androidx_test_junit"
61 | androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_ver"
62 | // Set this dependency to use JUnit 4 rules
63 | androidTestImplementation "androidx.test:rules:$androidx_test_rules"
64 | androidTestImplementation "androidx.test:core:$androidx_test_core"
65 | androidTestImplementation "androidx.test:core-ktx:$androidx_test_core"
66 |
67 | androidTestImplementation "org.mockito:mockito-core:$mockito_ver"
68 | androidTestImplementation "org.mockito:mockito-android:$mockito_ver"
69 | androidTestImplementation "com.noveogroup.android:android-logger:$android_logger_ver"
70 | androidTestImplementation "com.fasterxml.jackson.core:jackson-databind:$jacksonversion"
71 | }
72 |
--------------------------------------------------------------------------------
/user-profile/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
19 |
21 |
22 |
--------------------------------------------------------------------------------