├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── actions │ ├── build-docs │ │ └── action.yml │ ├── ci │ │ └── action.yml │ ├── publish-docs │ │ └── action.yml │ └── publish │ │ ├── action.yml │ │ └── publish.sh ├── pull_request_template.md └── workflows │ ├── ci.yml │ ├── lint-pr-title.yml │ ├── manual-publish-docs.yml │ ├── publish.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .release-please-manifest.json ├── .sdk_metadata.json ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── build.gradle ├── contract-tests ├── README.md ├── build.gradle └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── launchdarkly │ │ ├── sdk │ │ └── android │ │ │ └── ConfigHelper.java │ │ └── sdktest │ │ ├── Config.java │ │ ├── HookCallbackService.java │ │ ├── MainActivity.java │ │ ├── PrefixedLogAdapter.java │ │ ├── Representations.java │ │ ├── Router.java │ │ ├── SdkClientEntity.java │ │ ├── TestHook.java │ │ └── TestService.java │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── layout │ └── activity_main.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── example ├── build.gradle ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── launchdarkly │ │ └── example │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── launchdarkly-android-client-sdk ├── build.gradle ├── consumer-proguard-rules.pro └── src │ ├── androidTest │ ├── AndroidManifest.xml │ └── java │ │ └── com │ │ └── launchdarkly │ │ └── sdk │ │ └── android │ │ ├── AndroidLoggingRule.java │ │ ├── AndroidPlatformStateTest.java │ │ ├── AndroidTaskExecutorTest.java │ │ ├── AndroidTestUtil.java │ │ ├── EnvironmentReporterBuilderTest.java │ │ ├── LDClientEndToEndTest.java │ │ ├── LDClientEvaluationTest.java │ │ ├── LDClientEventTest.java │ │ ├── LDClientHooksTest.java │ │ ├── LDClientLoggingTest.java │ │ ├── LDClientTest.java │ │ ├── MultiEnvironmentLDClientTest.java │ │ ├── SharedPreferencesPersistentDataStoreTest.java │ │ ├── TestActivity.java │ │ └── TestDataWithLDClientTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ └── launchdarkly │ │ │ └── sdk │ │ │ └── android │ │ │ ├── AndroidPlatformState.java │ │ │ ├── AndroidTaskExecutor.java │ │ │ ├── AnonymousKeyContextModifier.java │ │ │ ├── AutoEnvContextModifier.java │ │ │ ├── ClientContextImpl.java │ │ │ ├── Components.java │ │ │ ├── ComponentsImpl.java │ │ │ ├── ConnectionInformation.java │ │ │ ├── ConnectionInformationState.java │ │ │ ├── ConnectivityManager.java │ │ │ ├── ContextDataManager.java │ │ │ ├── ContextIndex.java │ │ │ ├── DataModel.java │ │ │ ├── Debounce.java │ │ │ ├── EnvironmentData.java │ │ │ ├── EventUtil.java │ │ │ ├── FeatureFetcher.java │ │ │ ├── FeatureFlagChangeListener.java │ │ │ ├── HookRunner.java │ │ │ ├── HttpFeatureFlagFetcher.java │ │ │ ├── IContextModifier.java │ │ │ ├── LDAllFlagsListener.java │ │ │ ├── LDAndroidLogging.java │ │ │ ├── LDClient.java │ │ │ ├── LDClientInterface.java │ │ │ ├── LDConfig.java │ │ │ ├── LDFailure.java │ │ │ ├── LDFailureSerialization.java │ │ │ ├── LDFutures.java │ │ │ ├── LDHeaderUpdater.java │ │ │ ├── LDInvalidResponseCodeFailure.java │ │ │ ├── LDPackageConsts.java │ │ │ ├── LDStatusListener.java │ │ │ ├── LDTimberLogging.java │ │ │ ├── LDUtil.java │ │ │ ├── LaunchDarklyException.java │ │ │ ├── Migration.java │ │ │ ├── NoOpContextModifier.java │ │ │ ├── PersistentDataStoreWrapper.java │ │ │ ├── PlatformState.java │ │ │ ├── PollingDataSource.java │ │ │ ├── SharedPreferencesPersistentDataStore.java │ │ │ ├── StandardEndpoints.java │ │ │ ├── StreamingDataSource.java │ │ │ ├── TaskExecutor.java │ │ │ ├── env │ │ │ ├── AndroidEnvironmentReporter.java │ │ │ ├── ApplicationInfoEnvironmentReporter.java │ │ │ ├── EnvironmentReporterBuilder.java │ │ │ ├── EnvironmentReporterChainBase.java │ │ │ ├── IEnvironmentReporter.java │ │ │ └── SDKEnvironmentReporter.java │ │ │ ├── integrations │ │ │ ├── ApplicationInfoBuilder.java │ │ │ ├── EvaluationSeriesContext.java │ │ │ ├── EventProcessorBuilder.java │ │ │ ├── Hook.java │ │ │ ├── HookMetadata.java │ │ │ ├── HooksConfigurationBuilder.java │ │ │ ├── HttpConfigurationBuilder.java │ │ │ ├── IdentifySeriesContext.java │ │ │ ├── IdentifySeriesResult.java │ │ │ ├── PollingDataSourceBuilder.java │ │ │ ├── ServiceEndpointsBuilder.java │ │ │ ├── StreamingDataSourceBuilder.java │ │ │ ├── TestData.java │ │ │ ├── TrackSeriesContext.java │ │ │ └── package-info.java │ │ │ ├── interfaces │ │ │ ├── ServiceEndpoints.java │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── subsystems │ │ │ ├── ApplicationInfo.java │ │ │ ├── Callback.java │ │ │ ├── ClientContext.java │ │ │ ├── ComponentConfigurer.java │ │ │ ├── DataSource.java │ │ │ ├── DataSourceUpdateSink.java │ │ │ ├── DiagnosticDescription.java │ │ │ ├── EventProcessor.java │ │ │ ├── HookConfiguration.java │ │ │ ├── HttpConfiguration.java │ │ │ ├── PersistentDataStore.java │ │ │ └── package-info.java │ └── resources │ │ └── android-logger.properties │ └── test │ └── java │ ├── android │ └── util │ │ ├── Base64.java │ │ └── Pair.java │ └── com │ └── launchdarkly │ └── sdk │ └── android │ ├── AnonymousKeyContextModifierTest.java │ ├── AutoEnvContextModifierTest.java │ ├── ConnectivityManagerTest.java │ ├── ContextDataManagerContextCachingTest.java │ ├── ContextDataManagerFlagDataTest.java │ ├── ContextDataManagerListenersTest.java │ ├── ContextDataManagerTestBase.java │ ├── ContextIndexTest.java │ ├── DebounceTest.java │ ├── DiagnosticConfigTest.java │ ├── EnvironmentDataTest.java │ ├── FlagTest.java │ ├── HookRunnerTest.java │ ├── HttpConfigurationBuilderTest.java │ ├── LDAwaitFutureTest.java │ ├── LDCompletedFutureTest.java │ ├── LDConfigTest.java │ ├── LDFailureTest.java │ ├── LDUtilTest.java │ ├── MigrationTest.java │ ├── PersistentDataStoreWrapperTest.java │ ├── PollingDataSourceTest.java │ ├── StreamingDataSourceTest.java │ ├── TestDataTest.java │ └── integrations │ ├── ApplicationInfoBuilderTest.java │ └── HookConfigurationBuilderTest.java ├── release-please-config.json ├── scripts ├── release.sh ├── start-emulator.sh └── start-test-service.sh ├── settings.gradle ├── shared-test-code ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── launchdarkly │ └── sdk │ └── android │ ├── AssertHelpers.java │ ├── AwaitableCallback.java │ ├── DataSetBuilder.java │ ├── FlagBuilder.java │ ├── InMemoryPersistentDataStore.java │ ├── LogCaptureRule.java │ ├── MockComponents.java │ ├── MockPlatformState.java │ ├── NullPersistentDataStore.java │ ├── SimpleTestTaskExecutor.java │ └── TestUtil.java └── testharness-suppressions.txt /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the code in this library. If you're not sure whether the problem you are having is specifically related to this library, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/) and clicking "submit a request", or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://support.launchdarkly.com/hc/en-us/requests/new 5 | about: File your support requests with LaunchDarkly's support team 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/build-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Build Documentation 2 | description: 'Build Documentation.' 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - name: Build Documentation 8 | shell: bash 9 | run: ./gradlew javadoc 10 | -------------------------------------------------------------------------------- /.github/actions/ci/action.yml: -------------------------------------------------------------------------------- 1 | name: CI Workflow 2 | description: 'Shared CI workflow.' 3 | inputs: 4 | run_tests: 5 | description: 'If true, run unit tests, otherwise skip them.' 6 | required: false 7 | default: 'true' 8 | android_api_level: 9 | description: 'The Android API level to use.' 10 | required: true 11 | java_version: 12 | description: 'The Java version to use.' 13 | required: true 14 | java_distribution: 15 | description: 'The Java distribution to use.' 16 | required: false 17 | default: 'temurin' 18 | 19 | runs: 20 | using: composite 21 | steps: 22 | - name: Setup Java 23 | uses: actions/setup-java@v4 24 | with: 25 | distribution: ${{ inputs.java_distribution }} 26 | java-version: ${{ inputs.java_version }} 27 | 28 | - name: Setup Android SDK 29 | uses: android-actions/setup-android@v3 30 | 31 | - name: Build 32 | shell: bash 33 | id: build 34 | run: ./gradlew build jar 35 | 36 | - name: Run Unit Tests 37 | if: inputs.run_tests == 'true' 38 | shell: bash 39 | run: ./gradlew test 40 | 41 | - name: Build contract tests 42 | shell: bash 43 | run: make build-contract-tests 44 | 45 | - name: Perform Instrumented Tests 46 | uses: reactivecircus/android-emulator-runner@6b0df4b0efb23bb0ec63d881db79aefbc976e4b2 #2.30.1 47 | with: 48 | api-level: ${{ inputs.android_api_level }} 49 | target: google_apis 50 | emulator-boot-timeout: 900 51 | emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 52 | disable-animations: true 53 | script: | 54 | make start-contract-test-service 55 | make run-contract-tests 56 | ./gradlew connectedDebugAndroidTest 57 | 58 | - name: Build documentation 59 | uses: ./.github/actions/build-docs 60 | -------------------------------------------------------------------------------- /.github/actions/publish-docs/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | description: 'Publish the documentation to Github pages' 3 | inputs: 4 | token: 5 | description: 'Token to use for publishing.' 6 | required: true 7 | dry_run: 8 | description: 'Is this a dry run. If so no docs will be published.' 9 | required: true 10 | 11 | runs: 12 | using: composite 13 | steps: 14 | - uses: launchdarkly/gh-actions/actions/publish-pages@publish-pages-v1.0.1 15 | name: 'Publish to Github pages' 16 | if: ${{ inputs.dry_run == 'false' }} 17 | with: 18 | docs_path: launchdarkly-android-client-sdk/build/docs/javadoc 19 | github_token: ${{ inputs.token }} # For the shared action the token should be a GITHUB_TOKEN 20 | 21 | - name: Dry Run Publish Docs 22 | shell: bash 23 | if: ${{ inputs.dry_run == 'true' }} 24 | run: echo "Dry run. Not publishing docs." 25 | -------------------------------------------------------------------------------- /.github/actions/publish/action.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | description: 'Publish the package to Sonatype' 3 | inputs: 4 | dry_run: 5 | description: 'Is this a dry run. If so no package will be published.' 6 | required: true 7 | prerelease: 8 | description: 'Is this a prerelease. If so then it will be published to the staging repository only.' 9 | required: true 10 | signing_key_id: 11 | description: 'Signing key ID' 12 | required: true 13 | signing_key_passphrase: 14 | description: 'Signing key passphrase' 15 | required: true 16 | code_signing_keyring: 17 | description: 'The path of the code signing keyring.' 18 | required: true 19 | sonatype_username: 20 | description: 'Sonatype repo username.' 21 | required: true 22 | sonatype_password: 23 | description: 'Sonatype repo password.' 24 | required: true 25 | 26 | runs: 27 | using: composite 28 | steps: 29 | - name: Publish Library 30 | shell: bash 31 | if: ${{ inputs.dry_run == 'false' }} 32 | env: 33 | LD_RELEASE_IS_PRERELEASE: ${{ inputs.prerelease }} 34 | SIGNING_KEY_ID: ${{ inputs.signing_key_id }} 35 | SIGNING_KEY_PASSPHRASE: ${{ inputs.signing_key_passphrase }} 36 | SIGNING_SECRET_KEY_RING_FILE: ${{ inputs.code_signing_keyring }} 37 | SONATYPE_USER_NAME: ${{ inputs.sonatype_username }} 38 | SONATYPE_PASSWORD: ${{ inputs.sonatype_password }} 39 | run: source $GITHUB_ACTION_PATH/publish.sh 40 | 41 | - name: Dry Run Publish Library 42 | shell: bash 43 | if: ${{ inputs.dry_run == 'true' }} 44 | run: echo "Dry run. Not publishing." 45 | -------------------------------------------------------------------------------- /.github/actions/publish/publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ue 4 | 5 | echo "Publishing to Sonatype" 6 | if [ "${LD_RELEASE_IS_PRERELEASE}" == "true" ]; then 7 | echo "PRERELEASE" 8 | ./gradlew publishToSonatype -Psigning.keyId="${SIGNING_KEY_ID}" -Psigning.password="${SIGNING_KEY_PASSPHRASE}" -Psigning.secretKeyRingFile="${SIGNING_SECRET_KEY_RING_FILE}" -PsonatypeUsername="${SONATYPE_USER_NAME}" -PsonatypePassword="${SONATYPE_PASSWORD}" || { 9 | echo "Gradle publish/release failed" >&2 10 | exit 1 11 | } 12 | else 13 | echo "RELEASE" 14 | ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository -Psigning.keyId="${SIGNING_KEY_ID}" -Psigning.password="${SIGNING_KEY_PASSPHRASE}" -Psigning.secretKeyRingFile="${SIGNING_SECRET_KEY_RING_FILE}" -PsonatypeUsername="${SONATYPE_USER_NAME}" -PsonatypePassword="${SONATYPE_PASSWORD}" || { 15 | echo "Gradle publish/release failed" >&2 16 | exit 1 17 | } 18 | fi 19 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [ ] I have added test coverage for new or changed functionality 4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/main/CONTRIBUTING.md#submitting-pull-requests) 5 | - [ ] I have validated my changes against all supported platform versions 6 | 7 | **Related issues** 8 | 9 | Provide links to any issues in this repository or elsewhere relating to this pull request. 10 | 11 | **Describe the solution you've provided** 12 | 13 | Provide a clear and concise description of what you expect to happen. 14 | 15 | **Describe alternatives you've considered** 16 | 17 | Provide a clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: ['main'] 6 | paths-ignore: 7 | - '**.md' # Do not need to run CI for markdown changes. 8 | pull_request: 9 | branches: [ 'main' ] 10 | paths-ignore: 11 | - '**.md' 12 | 13 | jobs: 14 | ci-build: 15 | strategy: 16 | matrix: 17 | # TODO: Use full matrices 18 | # android_api_level: ['21','25','30','34'] 19 | # java_version: ['11', '17'] 20 | android_api_level: ['25'] 21 | java_version: ['17'] 22 | runs-on: ubuntu-22.04 23 | 24 | steps: 25 | # This enables hardware acceleration on large linux runners 26 | - name: Enable KVM group perms 27 | run: | 28 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 29 | sudo udevadm control --reload-rules 30 | sudo udevadm trigger --name-match=kvm 31 | 32 | - uses: actions/checkout@v4 33 | - uses: ./.github/actions/ci 34 | with: 35 | android_api_level: ${{ matrix.android_api_level }} 36 | java_version: ${{ matrix.java_version }} 37 | -------------------------------------------------------------------------------- /.github/workflows/lint-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: Lint PR title 2 | 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | 10 | jobs: 11 | lint-pr-title: 12 | uses: launchdarkly/gh-actions/.github/workflows/lint-pr-title.yml@main 13 | -------------------------------------------------------------------------------- /.github/workflows/manual-publish-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: 3 | 4 | name: Publish Docs 5 | jobs: 6 | build-publish: 7 | runs-on: ubuntu-22.04 8 | permissions: 9 | id-token: write # Needed if using OIDC to get release secrets. 10 | contents: write # Needed in this case to write github pages. 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.0.1 15 | name: Assume aws role 16 | with: 17 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 18 | 19 | - name: Build and Test 20 | uses: ./.github/actions/ci 21 | 22 | - name: Publish Documentation 23 | uses: ./.github/actions/publish-docs 24 | with: 25 | token: ${{secrets.GITHUB_TOKEN}} 26 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | run_tests: 6 | description: 'If true, run unit tests, otherwise skip them.' 7 | type: boolean 8 | default: true 9 | dry_run: 10 | description: 'Is this a dry run. If so no package will be published.' 11 | type: boolean 12 | required: true 13 | prerelease: 14 | description: 'If true, then this is a prerelease and should be published to the staging repository only.' 15 | type: boolean 16 | required: true 17 | workflow_call: 18 | inputs: 19 | run_tests: 20 | description: 'If true, run unit tests, otherwise skip them.' 21 | required: false 22 | type: boolean 23 | default: true 24 | dry_run: 25 | description: 'Is this a dry run. If so no package will be published.' 26 | type: boolean 27 | required: true 28 | prerelease: 29 | description: 'If true, then this is a prerelease and should be published to the staging repository only.' 30 | type: boolean 31 | required: true 32 | 33 | jobs: 34 | build-and-publish: 35 | runs-on: ubuntu-22.04 36 | permissions: 37 | id-token: write 38 | contents: write # Needed in this case to write github pages. 39 | steps: 40 | # This enables hardware acceleration on large linux runners 41 | - name: Enable KVM group perms 42 | run: | 43 | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules 44 | sudo udevadm control --reload-rules 45 | sudo udevadm trigger --name-match=kvm 46 | 47 | - uses: actions/checkout@v4 48 | 49 | - name: CI check 50 | uses: ./.github/actions/ci 51 | with: 52 | run_tests: ${{ inputs.run_tests }} 53 | android_api_level: '25' 54 | java_version: '17' 55 | 56 | - uses: launchdarkly/gh-actions/actions/release-secrets@release-secrets-v1.1.0 57 | name: Get secrets 58 | with: 59 | aws_assume_role: ${{ vars.AWS_ROLE_ARN }} 60 | ssm_parameter_pairs: '/production/common/releasing/sonatype/username = SONATYPE_USER_NAME, 61 | /production/common/releasing/sonatype/password = SONATYPE_PASSWORD, 62 | /production/common/releasing/android_code_signing/private_key_id = SIGNING_KEY_ID, 63 | /production/common/releasing/android_code_signing/private_key_passphrase = SIGNING_KEY_PASSPHRASE' 64 | s3_path_pairs: 'launchdarkly-releaser/android/code-signing-keyring.gpg = code-signing-keyring.gpg' 65 | 66 | - name: Publish 67 | uses: ./.github/actions/publish 68 | with: 69 | dry_run: ${{ inputs.dry_run }} 70 | prerelease: ${{ inputs.prerelease }} 71 | signing_key_id: ${{ env.SIGNING_KEY_ID }} 72 | signing_key_passphrase: ${{ env.SIGNING_KEY_PASSPHRASE }} 73 | code_signing_keyring: ${{ github.workspace }}/code-signing-keyring.gpg 74 | sonatype_username: ${{ env.SONATYPE_USER_NAME }} 75 | sonatype_password: ${{ env.SONATYPE_PASSWORD }} 76 | 77 | - name: Publish Documentation 78 | uses: ./.github/actions/publish-docs 79 | with: 80 | dry_run: ${{ inputs.dry_run }} 81 | token: ${{secrets.GITHUB_TOKEN}} 82 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Run Release Please 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main 8 | 9 | jobs: 10 | release-please: 11 | runs-on: ubuntu-22.04 12 | 13 | permissions: 14 | id-token: write # Needed for OIDC to get release secrets. 15 | contents: write # Contents and pull-requests are for release-please to make releases. 16 | pull-requests: write 17 | 18 | outputs: 19 | releases_created: ${{ steps.release.outputs.releases_created }} 20 | 21 | steps: 22 | - uses: google-github-actions/release-please-action@v4 23 | id: release 24 | with: 25 | token: ${{ secrets.GITHUB_TOKEN }} 26 | target-branch: ${{ github.ref_name }} 27 | 28 | call-workflow-publish: 29 | needs: release-please 30 | uses: ./.github/workflows/publish.yml 31 | if: ${{ needs.release-please.outputs.releases_created == 'true' }} 32 | with: 33 | run_tests: true 34 | dry_run: false 35 | prerelease: false 36 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | # Happen once per day at 1:30 AM 6 | - cron: '30 1 * * *' 7 | 8 | jobs: 9 | sdk-close-stale: 10 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | .DS_Store 4 | build 5 | /captures 6 | .project 7 | 8 | *.iml 9 | .idea 10 | .idea/* 11 | .classpath 12 | .project 13 | .settings/ 14 | .vscode/ 15 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "5.8.0" 3 | } 4 | -------------------------------------------------------------------------------- /.sdk_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "sdks": { 4 | "android": { 5 | "name": "Android SDK", 6 | "type": "client-side", 7 | "languages": [ 8 | "Java", "Kotlin" 9 | ], 10 | "userAgents": ["AndroidClient"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository Maintainers 2 | * @launchdarkly/team-sdk-java 3 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to the LaunchDarkly SDK for Android 2 | 3 | LaunchDarkly has published an [SDK contributor's guide](https://docs.launchdarkly.com/sdk/concepts/contributors-guide) that provides a detailed explanation of how our SDKs work. See below for additional information on how to contribute to this SDK. 4 | 5 | ## Submitting bug reports and feature requests 6 | 7 | The LaunchDarkly SDK team monitors the [issue tracker](https://github.com/launchdarkly/android-client-sdk/issues) in the SDK repository. Bug reports and feature requests specific to this SDK should be filed in this issue tracker. The SDK team will respond to all newly filed issues within two business days. 8 | 9 | ## Submitting pull requests 10 | 11 | We encourage pull requests and other contributions from the community. Before submitting pull requests, ensure that all temporary or unintended code is removed. Don't worry about adding reviewers to the pull request; the LaunchDarkly SDK team will add themselves. The SDK team will acknowledge all pull requests within two business days. 12 | 13 | ## Build instructions 14 | 15 | ### Prerequisites 16 | 17 | 1. Download and install [Android Studio](https://developer.android.com/studio/index.html) 18 | 1. Open it and follow setup prompts. You'll at least want API versions 23 and 24. 19 | 1. Open the android-client-sdk project in Android Studio. 20 | 1. Enter your mobile key in MainActivity.java 21 | 22 | ### Building and running 23 | 24 | To build and run, there should be a "Run" green triangle button in the Android Studio UI for your app. Click it. 25 | 26 | ### Testing 27 | 28 | To run all tests, there should be a "Run tests" green triangle button in the Android Studio UI. Click it. 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Catamorphic, Co. 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 | http://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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TEST_HARNESS_PARAMS= -skip "events/disabling" -status-timeout 60 2 | # can add temporary test skips etc. here 3 | # Currently we are skipping the "events/disabling" tests because the Android SDK has no way to 4 | # disable events. That wasn't an issue earlier because the "events/disabling" tests were getting 5 | # automatically skipped by sdk-test-harness for a different reason: they rely on the 6 | # ServiceEndpoints API, which the Android SDK didn't previously support. 7 | 8 | build-contract-tests: 9 | @cd contract-tests && ../gradlew --no-daemon -s assembleDebug -PdisablePreDex 10 | 11 | start-emulator: 12 | @scripts/start-emulator.sh 13 | 14 | start-contract-test-service: 15 | @scripts/start-test-service.sh 16 | 17 | run-contract-tests: 18 | @curl -s https://raw.githubusercontent.com/launchdarkly/sdk-test-harness/v1.0.0/downloader/run.sh \ 19 | | VERSION=v2 PARAMS="-url http://localhost:8001 -host 10.0.2.2 -skip-from testharness-suppressions.txt -debug $(TEST_HARNESS_PARAMS)" sh 20 | 21 | contract-tests: build-contract-tests start-emulator start-contract-test-service run-contract-tests 22 | 23 | .PHONY: build-contract-tests start-emulator start-contract-test-service run-contract-tests contract-tests 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LaunchDarkly SDK for Android 2 | ![Actions Status](https://github.com/launchdarkly/android-client-sdk/actions/workflows/ci.yml/badge.svg) 3 | 4 | ## LaunchDarkly overview 5 | 6 | [LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today! 7 | 8 | [![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly) 9 | 10 | ## Supported Android versions 11 | 12 | This version of the LaunchDarkly SDK has been tested with Android SDK versions 21 and up (5.0 Lollipop). 13 | 14 | ## Getting started 15 | 16 | Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/android#getting-started) for instructions on getting started with using the SDK. 17 | 18 | ## Learn more 19 | 20 | Read our [documentation](https://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/client-side/android) or our [Javadocs](http://launchdarkly.github.io/android-client-sdk/). 21 | 22 | ## Testing 23 | 24 | We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly. 25 | 26 | ## Contributing 27 | 28 | We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK. 29 | 30 | ## About LaunchDarkly 31 | 32 | * LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: 33 | * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. 34 | * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). 35 | * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. 36 | * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline. 37 | * LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. 38 | * Explore LaunchDarkly 39 | * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information 40 | * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides 41 | * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation 42 | * [launchdarkly.com/blog](https://launchdarkly.com/blog/ "LaunchDarkly Blog Documentation") for the latest product updates 43 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting and Fixing Security Issues 2 | 3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. 4 | 5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import java.time.Duration 2 | 3 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 4 | 5 | buildscript { 6 | repositories { 7 | mavenCentral() 8 | google() 9 | } 10 | dependencies { 11 | classpath('com.android.tools.build:gradle:7.1.1') 12 | 13 | // For displaying method/field counts when building with Gradle: 14 | // https://github.com/KeepSafe/dexcount-gradle-plugin 15 | classpath("com.getkeepsafe.dexcount:dexcount-gradle-plugin:3.1.0") 16 | 17 | // NOTE: Do not place your application dependencies here; they belong 18 | // in the individual module build.gradle files 19 | } 20 | } 21 | 22 | plugins { 23 | id("java") 24 | id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 25 | } 26 | 27 | // Must be specified in root project for the gradle nexus publish plugin. 28 | group = "com.launchdarkly" 29 | // Specified in the root project so Releaser's `publish-dry-run.sh` can see it in `./gradlew properties` 30 | archivesBaseName = 'launchdarkly-android-client-sdk' 31 | // Specified in gradle.properties 32 | version = version 33 | 34 | allprojects { 35 | repositories { 36 | mavenLocal() 37 | // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: 38 | maven { url = uri("https://oss.sonatype.org/content/groups/public/") } 39 | google() 40 | mavenCentral() 41 | } 42 | } 43 | 44 | subprojects { 45 | afterEvaluate { 46 | configure(android.lintOptions) { 47 | abortOnError = false 48 | } 49 | configure(android.compileOptions) { 50 | sourceCompatibility = JavaVersion.VERSION_1_8 51 | targetCompatibility = JavaVersion.VERSION_1_8 52 | } 53 | gradle.projectsEvaluated { 54 | tasks.withType(JavaCompile) { 55 | // enable deprecation checks 56 | options.compilerArgs << "-Xlint:deprecation" 57 | } 58 | } 59 | } 60 | } 61 | 62 | nexusPublishing { 63 | packageGroup = "com.launchdarkly" 64 | repositories { 65 | sonatype() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /contract-tests/README.md: -------------------------------------------------------------------------------- 1 | # contract-tests service 2 | 3 | A test service that runs inside an Android emulator, that follows the [contract-tests specification](https://github.com/launchdarkly/sdk-test-harness/blob/main/docs/service_spec.md). 4 | 5 | ## Running locally 6 | 7 | You can run the contract tests locally via the `Makefile`, which will: 8 | * start up an emulator 9 | * run the contract-test service APK in the emulator 10 | * forward port 8001 (using `adb forward`) so that we can hit the HTTP server running inside the emulator 11 | * important: this part is something you can't do with Android Studio, which is why I don't use Android Studio to run the contract tests 12 | * run the [SDK test harness](https://github.com/launchdarkly/sdk-test-harness) against the service 13 | 14 | To run the contract tests and all its dependencies at once: 15 | ```sh 16 | $ make contract-tests 17 | ``` 18 | 19 | For a pseudo-interactive dev flow, I use [watchexec](https://github.com/watchexec/watchexec): 20 | ```sh 21 | # start up the emulator once 22 | $ make start-emulator 23 | # anytime code changes, rebuild and restart the test service 24 | $ watchexec make build-contract-tests start-contract-test-service 25 | # meanwhile, run the test harness whenever you want 26 | $ make run-contract-tests 27 | ``` 28 | 29 | For this to work, there are some prerequisites that must be on your machine: 30 | 31 | ### Install Android Studio 32 | 33 | Even though we won't use it to run the contract tests, installing Android Studio will give us all the ingredients necessary for things to work. 34 | 35 | ### [Android command-line tools](https://developer.android.com/studio/command-line) 36 | 37 | The following programs should already be present thanks to Android Studio, but need to be 38 | available on your `$PATH` to be run at the command line: 39 | 40 | * `adb` 41 | * `avdmanager` 42 | * `emulator` 43 | * `sdkmanager` 44 | 45 | This is what my `~/.zshrc` looks like, to make that happen: 46 | 47 | ```sh 48 | export PATH="/Users/alex/Library/Android/sdk/cmdline-tools/latest/bin:$PATH" # avdmanager, sdkmanager 49 | export PATH="/Users/alex/Library/Android/sdk/platform-tools/:$PATH" # adb 50 | export PATH="/Users/alex/Library/Android/sdk/emulator/:$PATH" # emulator 51 | ``` 52 | 53 | ### System images 54 | 55 | The contract-tests scripts expect at least one Android system image (i.e. an image of Android OS that can be run as an emulator) 56 | to be installed. If you've done any Android development already, there should be at least one that works. `start-emulator.sh` 57 | will automatically pick the latest applicable image when creating the emulator. 58 | 59 | You can verify by calling `sdkmanager --list_installed`. You're looking for something like: 60 | ``` 61 | Path | Version | Description | Location 62 | ------- | ------- | ------- | ------- 63 | ... ... ... ... 64 | system-images;android-32;google_apis;arm64-v8a | 3 | Google APIs ARM 64 v8a System Image | system-images/android-32/google_apis/arm64-v8a 65 | ``` 66 | but swap `arm64-v8a` with `x86` if you aren't on an M1 machine. -------------------------------------------------------------------------------- /contract-tests/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | // make sure this line comes *after* you apply the Android plugin 4 | id("com.getkeepsafe.dexcount") 5 | } 6 | 7 | android { 8 | compileSdkVersion(30) 9 | buildToolsVersion = "30.0.3" 10 | 11 | defaultConfig { 12 | applicationId = "com.launchdarkly.sdktest" 13 | minSdkVersion(21) 14 | targetSdkVersion(30) 15 | versionCode = 1 16 | versionName = "1.0" 17 | } 18 | 19 | buildTypes { 20 | release { 21 | minifyEnabled = true 22 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | } 26 | 27 | dependencies { 28 | // https://mvnrepository.com/artifact/org.nanohttpd/nanohttpd 29 | implementation("org.nanohttpd:nanohttpd:2.3.1") 30 | implementation("com.google.code.gson:gson:2.8.9") 31 | implementation("com.squareup.okhttp3:okhttp:4.9.2") 32 | implementation(project(":launchdarkly-android-client-sdk")) 33 | // Comment the previous line and uncomment this one to depend on the published artifact: 34 | //implementation("com.launchdarkly:launchdarkly-android-client-sdk:3.1.5") 35 | 36 | implementation(project(":shared-test-code")) 37 | } 38 | -------------------------------------------------------------------------------- /contract-tests/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 16 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdk/android/ConfigHelper.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | public class ConfigHelper { 4 | // This method exists because Config.Builder.persistentDataStore is currently package-private - 5 | // using a custom persistence component is not yet part of the public API. 6 | public static LDConfig.Builder configureIsolatedInMemoryPersistence(LDConfig.Builder builder) { 7 | builder.persistentDataStore(new InMemoryPersistentDataStore()); 8 | return builder; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/Config.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import android.os.Bundle; 4 | 5 | /** 6 | * Contains the configuration values passed to the test service at runtime. 7 | * Currently only contains the port number. 8 | */ 9 | public class Config { 10 | public int port = 35001; 11 | 12 | public static Config fromArgs(Bundle params) { 13 | Config ret = new Config(); 14 | 15 | String portStr = params == null ? null : params.getString("PORT"); 16 | if (portStr != null) { 17 | ret.port = Integer.parseInt(portStr); 18 | } 19 | 20 | return ret; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/HookCallbackService.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import okhttp3.MediaType; 4 | import okhttp3.Request; 5 | import okhttp3.RequestBody; 6 | import okhttp3.Response; 7 | 8 | import java.net.URI; 9 | 10 | public class HookCallbackService { 11 | private final URI serviceUri; 12 | 13 | public HookCallbackService(URI serviceUri) { 14 | this.serviceUri = serviceUri; 15 | } 16 | 17 | public void post(Object params) { 18 | RequestBody body = RequestBody.create( 19 | TestService.gson.toJson(params == null ? "{}" : params), 20 | MediaType.parse("application/json") 21 | ); 22 | Request request = new Request.Builder().url(serviceUri.toString()) 23 | .method("POST", body) 24 | .build(); 25 | try (Response response = TestService.client.newCall(request).execute()) { 26 | assertOk(response); 27 | } catch (Exception e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | 32 | private void assertOk(Response response) { 33 | if (!response.isSuccessful()) { 34 | String body = ""; 35 | if (response.body() != null) { 36 | try { 37 | body = ": " + response.body().string(); 38 | } catch (Exception e) {} 39 | } 40 | throw new RuntimeException("HTTP error " + response.code() + " from callback to " + serviceUri + body); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.widget.TextView; 6 | 7 | import com.launchdarkly.logging.LDLogger; 8 | import com.launchdarkly.sdk.android.LDAndroidLogging; 9 | 10 | import java.io.IOException; 11 | 12 | public class MainActivity extends Activity { 13 | private Config config; 14 | private TestService server; 15 | private LDLogger logger = LDLogger.withAdapter(LDAndroidLogging.adapter(), "MainActivity"); 16 | 17 | @Override 18 | protected void onCreate(Bundle savedInstanceState) { 19 | super.onCreate(savedInstanceState); 20 | setContentView(R.layout.activity_main); 21 | config = Config.fromArgs(getIntent().getExtras()); 22 | 23 | TextView textIpaddr = findViewById(R.id.ipaddr); 24 | textIpaddr.setText("Contract test service running on port " + config.port); 25 | } 26 | 27 | @Override 28 | public void onStop() { 29 | super.onStop(); 30 | if (server != null) { 31 | server.stop(); 32 | } 33 | } 34 | 35 | @Override 36 | protected void onResume() { 37 | super.onResume(); 38 | logger.warn("Restarting test service on port {}", config.port); 39 | server = new TestService(getApplication()); 40 | if (!server.isAlive()) { 41 | try { 42 | server.start(); 43 | } catch (IOException e) { 44 | logger.error("Error starting server: {}", e); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/PrefixedLogAdapter.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import com.launchdarkly.logging.LDLogAdapter; 4 | import com.launchdarkly.logging.LDLogLevel; 5 | 6 | /** 7 | * This wraps the underlying logging adapter to use the logger name as a prefix for the message 8 | * text, rather than passing it through as a tag, because Android logging tags have a length 9 | * limit so we can't put the whole test name there. 10 | */ 11 | public class PrefixedLogAdapter implements LDLogAdapter { 12 | private final LDLogAdapter wrappedAdapter; 13 | private final String singleLoggerName; 14 | 15 | public PrefixedLogAdapter(LDLogAdapter wrappedAdapter, String singleLoggerName) { 16 | this.wrappedAdapter = wrappedAdapter; 17 | this.singleLoggerName = singleLoggerName; 18 | } 19 | 20 | @Override 21 | public Channel newChannel(String name) { 22 | return new ChannelImpl(wrappedAdapter.newChannel(singleLoggerName), 23 | "[" + name + "] "); 24 | } 25 | 26 | private static final class ChannelImpl implements Channel { 27 | private final Channel wrappedChannel; 28 | private final String prefix; 29 | 30 | public ChannelImpl(Channel wrappedChannel, String prefix) { 31 | this.wrappedChannel = wrappedChannel; 32 | this.prefix = prefix; 33 | } 34 | 35 | @Override 36 | public boolean isEnabled(LDLogLevel level) { 37 | return wrappedChannel.isEnabled(level); 38 | } 39 | 40 | @Override 41 | public void log(LDLogLevel level, Object message) { 42 | wrappedChannel.log(level, prefix + message.toString()); 43 | } 44 | 45 | @Override 46 | public void log(LDLogLevel level, String format, Object param) { 47 | wrappedChannel.log(level, prefix + format, param); 48 | } 49 | 50 | @Override 51 | public void log(LDLogLevel level, String format, Object param1, Object param2) { 52 | wrappedChannel.log(level, prefix + format, param1, param2); 53 | } 54 | 55 | @Override 56 | public void log(LDLogLevel level, String format, Object... params) { 57 | wrappedChannel.log(level, prefix + format, params); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/Router.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.regex.Matcher; 6 | import java.util.regex.Pattern; 7 | 8 | import fi.iki.elonen.NanoHTTPD; 9 | 10 | import static fi.iki.elonen.NanoHTTPD.newFixedLengthResponse; 11 | 12 | /** 13 | * An HTTP router inspired by com.launchdarkly.testhelpers.httptest.SimpleRouter, that 14 | * we can use with NanoHTTPD. 15 | * https://github.com/launchdarkly/java-test-helpers/blob/main/src/main/java/com/launchdarkly/testhelpers/httptest/SimpleRouter.java 16 | */ 17 | public class Router { 18 | public interface Handler { 19 | NanoHTTPD.Response apply(List pathParams, String body) throws Exception; 20 | } 21 | 22 | private final List routes = new ArrayList<>(); 23 | 24 | private static class Route { 25 | final String method; 26 | final Pattern pattern; 27 | final Handler handler; 28 | 29 | Route(String method, Pattern pattern, Handler handler) { 30 | this.method = method; 31 | this.pattern = pattern; 32 | this.handler = handler; 33 | } 34 | } 35 | 36 | public void add(String method, String path, Handler handler) { 37 | addRegex(method, Pattern.compile(Pattern.quote(path)), handler); 38 | } 39 | 40 | public void addRegex(String method, Pattern regex, Handler handler) { 41 | routes.add(new Route(method, regex, handler)); 42 | } 43 | 44 | public NanoHTTPD.Response route(String method, String path, String body) throws Exception { 45 | boolean matchedPath = false; 46 | for (Route r: routes) { 47 | Matcher m = r.pattern.matcher(path); 48 | if (m.matches()) { 49 | matchedPath = true; 50 | if (r.method != null && !r.method.equalsIgnoreCase(method)) { 51 | continue; 52 | } 53 | ArrayList params = new ArrayList<>(); 54 | if (m.groupCount() > 0) { 55 | for (int i = 1; i <= m.groupCount(); i++) { 56 | params.add(m.group(i)); 57 | } 58 | } 59 | return r.handler.apply(params, body); 60 | } 61 | } 62 | if (matchedPath) { 63 | return newFixedLengthResponse(NanoHTTPD.Response.Status.METHOD_NOT_ALLOWED, NanoHTTPD.MIME_PLAINTEXT, "Method Not Allowed\n"); 64 | } else { 65 | return newFixedLengthResponse(NanoHTTPD.Response.Status.NOT_FOUND, NanoHTTPD.MIME_PLAINTEXT, "Not Found\n"); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /contract-tests/src/main/java/com/launchdarkly/sdktest/TestHook.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdktest; 2 | 3 | import com.launchdarkly.sdk.EvaluationDetail; 4 | import com.launchdarkly.sdk.LDValue; 5 | import com.launchdarkly.sdk.android.integrations.EvaluationSeriesContext; 6 | import com.launchdarkly.sdk.android.integrations.Hook; 7 | 8 | import com.launchdarkly.sdk.android.integrations.TrackSeriesContext; 9 | import com.launchdarkly.sdktest.Representations.EvaluationSeriesCallbackParams; 10 | import com.launchdarkly.sdktest.Representations.TrackSeriesCallbackParams; 11 | import com.launchdarkly.sdktest.Representations.HookData; 12 | import com.launchdarkly.sdktest.Representations.HookErrors; 13 | 14 | import java.util.Collections; 15 | import java.util.HashMap; 16 | import java.util.Map; 17 | 18 | public class TestHook extends Hook { 19 | private final HookCallbackService callbackService; 20 | private final HookData hookData; 21 | private final HookErrors hookErrors; 22 | 23 | public TestHook(String name, HookCallbackService callbackService, HookData data, HookErrors errors) { 24 | super(name); 25 | this.callbackService = callbackService; 26 | this.hookData = data; 27 | this.hookErrors = errors; 28 | } 29 | 30 | @Override 31 | public Map beforeEvaluation(EvaluationSeriesContext seriesContext, Map data) { 32 | if (hookErrors.beforeEvaluation != null) { 33 | throw new RuntimeException(hookErrors.beforeEvaluation); 34 | } 35 | 36 | EvaluationSeriesCallbackParams params = new EvaluationSeriesCallbackParams(); 37 | params.evaluationSeriesContext = seriesContext; 38 | params.evaluationSeriesData = data; 39 | params.stage = "beforeEvaluation"; 40 | 41 | callbackService.post(params); 42 | 43 | Map newData = new HashMap<>(data); 44 | if (hookData.beforeEvaluation != null) { 45 | newData.putAll(hookData.beforeEvaluation); 46 | } 47 | 48 | return Collections.unmodifiableMap(newData); 49 | } 50 | 51 | @Override 52 | public Map afterEvaluation(EvaluationSeriesContext seriesContext, Map data, EvaluationDetail evaluationDetail) { 53 | if (hookErrors.afterEvaluation != null) { 54 | throw new RuntimeException(hookErrors.afterEvaluation); 55 | } 56 | 57 | EvaluationSeriesCallbackParams params = new EvaluationSeriesCallbackParams(); 58 | params.evaluationSeriesContext = seriesContext; 59 | params.evaluationSeriesData = data; 60 | params.evaluationDetail = evaluationDetail; 61 | params.stage = "afterEvaluation"; 62 | 63 | callbackService.post(params); 64 | 65 | Map newData = new HashMap<>(); 66 | if (hookData.afterEvaluation != null) { 67 | newData.putAll(hookData.afterEvaluation); 68 | } 69 | 70 | return Collections.unmodifiableMap(newData); 71 | } 72 | 73 | @Override 74 | public void afterTrack(TrackSeriesContext seriesContext) { 75 | if (hookErrors.afterTrack != null) { 76 | throw new RuntimeException(hookErrors.afterTrack); 77 | } 78 | 79 | TrackSeriesCallbackParams params = new TrackSeriesCallbackParams(); 80 | params.trackSeriesContext = seriesContext; 81 | params.stage = "afterTrack"; 82 | 83 | callbackService.post(params); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/contract-tests/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /contract-tests/src/main/res/values-w820dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 64dp 6 | 7 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16dp 4 | 16dp 5 | 6 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | android-client-restwrapper 3 | 4 | -------------------------------------------------------------------------------- /contract-tests/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | #x-release-please-start-version 21 | version=5.8.0 22 | #x-release-please-end 23 | 24 | sonatypeUsername= 25 | sonatypePassword= 26 | githubUser= 27 | githubPassword= 28 | 29 | android.useAndroidX=true 30 | org.gradle.jvmargs=-Xmx4608M -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Jan 11 09:20:17 PST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/consumer-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # LaunchDarkly Android SDK proguard rules 2 | # 3 | # For more details, see https://developer.android.com/studio/build/shrink-code 4 | # 5 | # Basically, at a minimum we *must* include here: 6 | # 7 | # - Any class that will be serialized or deserialized with Gson. That is, if we are 8 | # ever passing this class to Gson.fromJson() or Gson.toJson(), it must appear here. 9 | # We do not need to include it if we are just using the Gson streaming API directly 10 | # to read or write properties of that class, so that Gson itself does not actually 11 | # interact with the class. 12 | # 13 | # - Any class that is referenced in Gson serialization-related annotations. 14 | 15 | -keep class com.launchdarkly.sdk.AttributeRef { ; } 16 | -keep class com.launchdarkly.sdk.LDContext { ; } 17 | -keep class com.launchdarkly.sdk.LDValue { ; } 18 | -keep class * extends com.launchdarkly.sdk.LDValue { ; } 19 | -keep class com.launchdarkly.sdk.EvaluationReason { ; } 20 | -keep enum com.launchdarkly.sdk.EvaluationReason$* { *; } 21 | -keep class com.launchdarkly.sdk.EvaluationDetail { ; } 22 | 23 | -keep enum com.launchdarkly.sdk.android.ConnectionInformation$ConnectionMode { *; } 24 | -keep class com.launchdarkly.sdk.android.ConnectionInformationState { ; } 25 | -keep class com.launchdarkly.sdk.android.DataModel$Flag { ; } 26 | -keep class com.launchdarkly.sdk.android.EnvironmentData { ; } 27 | -keep class com.launchdarkly.sdk.android.LDFailure { ; } 28 | -keep class com.launchdarkly.sdk.android.LDInvalidResponseCodeFailure { ; } 29 | -keep class com.launchdarkly.sdk.android.StreamUpdateProcessor$DeleteMessage { ; } 30 | 31 | -keep class * extends com.google.gson.TypeAdapter 32 | -keep class * implements com.google.gson.TypeAdapterFactory 33 | -keep class * implements com.google.gson.JsonSerializer 34 | -keep class * implements com.google.gson.JsonDeserializer 35 | -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken 36 | -keep,allowobfuscation,allowshrinking class * extends com.google.gson.reflect.TypeToken 37 | 38 | -dontwarn org.conscrypt.ConscryptHostnameVerifier 39 | 40 | -keepattributes *Annotation*,Signature,InnerClasses 41 | 42 | # Usually, these should be redundant with directives added by AAPT. We include them 43 | # to be more explicit about what to save in case of build configurations having 44 | # different default directives included. 45 | -keep class com.launchdarkly.sdk.android.ConnectivityReceiver { (); } 46 | -keep class com.launchdarkly.sdk.android.PollingUpdater { (); } 47 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | 15 | 16 | 17 | 20 | 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/AndroidLoggingRule.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.logging.LDLogAdapter; 4 | import com.launchdarkly.logging.LDLogLevel; 5 | import com.launchdarkly.logging.LDLogger; 6 | 7 | import org.junit.rules.TestWatcher; 8 | import org.junit.runner.Description; 9 | 10 | import timber.log.Timber; 11 | 12 | /** 13 | * Adding this rule to a test provides an {@link LDLogger} that writes to the device logs, tagged 14 | * with the name of the current test (although the name may be truncated due to platform limits on 15 | * log tag length). 16 | */ 17 | public class AndroidLoggingRule extends TestWatcher { 18 | // This length limit exists in Android API 25 and above. 19 | private static final int ANDROID_MAX_LOG_TAG_LENGTH = 23; 20 | 21 | public LDLogAdapter logAdapter; 22 | public String loggerName; 23 | public LDLogger logger; 24 | 25 | public AndroidLoggingRule() {} 26 | 27 | protected void starting(Description description) { 28 | logAdapter = new PrefixedLogAdapter(LDAndroidLogging.adapter(true), "SdkTest"); 29 | loggerName = description.getMethodName(); 30 | logger = LDLogger.withAdapter(logAdapter, loggerName); 31 | } 32 | 33 | /** 34 | * This wraps the underlying logging adapter to use the logger name as a prefix for the message 35 | * text, rather than passing it through as a tag, because Android logging tags have a length 36 | * limit so we can't put the whole test name there. 37 | */ 38 | static final class PrefixedLogAdapter implements LDLogAdapter { 39 | private final LDLogAdapter wrappedAdapter; 40 | private final String singleLoggerName; 41 | 42 | public PrefixedLogAdapter(LDLogAdapter wrappedAdapter, String singleLoggerName) { 43 | this.wrappedAdapter = wrappedAdapter; 44 | this.singleLoggerName = singleLoggerName; 45 | } 46 | 47 | @Override 48 | public Channel newChannel(String name) { 49 | return new ChannelImpl(wrappedAdapter.newChannel(singleLoggerName), 50 | "[" + name + "] "); 51 | } 52 | 53 | private static final class ChannelImpl implements Channel { 54 | private final Channel wrappedChannel; 55 | private final String prefix; 56 | 57 | public ChannelImpl(Channel wrappedChannel, String prefix) { 58 | this.wrappedChannel = wrappedChannel; 59 | this.prefix = prefix; 60 | } 61 | 62 | @Override 63 | public boolean isEnabled(LDLogLevel level) { 64 | return wrappedChannel.isEnabled(level); 65 | } 66 | 67 | @Override 68 | public void log(LDLogLevel level, Object message) { 69 | wrappedChannel.log(level, prefix + message.toString()); 70 | } 71 | 72 | @Override 73 | public void log(LDLogLevel level, String format, Object param) { 74 | wrappedChannel.log(level, prefix + format, param); 75 | } 76 | 77 | @Override 78 | public void log(LDLogLevel level, String format, Object param1, Object param2) { 79 | wrappedChannel.log(level, prefix + format, param1, param2); 80 | } 81 | 82 | @Override 83 | public void log(LDLogLevel level, String format, Object... params) { 84 | wrappedChannel.log(level, prefix + format, params); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/AndroidTestUtil.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import androidx.test.ext.junit.rules.ActivityScenarioRule; 4 | 5 | import java.util.function.Consumer; 6 | 7 | public class AndroidTestUtil { 8 | public static void doSynchronouslyOnMainThreadForTestScenario( 9 | ActivityScenarioRule testScenario, 10 | Consumer action 11 | ) { 12 | testScenario.getScenario().onActivity(act -> { 13 | TestUtil.doSynchronouslyOnNewThread(() -> { 14 | action.accept(act); 15 | }); 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EnvironmentReporterBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.test.core.app.ApplicationProvider; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; 9 | import com.launchdarkly.sdk.android.env.IEnvironmentReporter; 10 | import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; 11 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 12 | 13 | import org.junit.Assert; 14 | import org.junit.Test; 15 | import org.junit.runner.RunWith; 16 | 17 | @RunWith(AndroidJUnit4.class) 18 | public class EnvironmentReporterBuilderTest { 19 | 20 | /** 21 | * Requirement 1.2.5.2 - Prioritized sourcing application info attributes 22 | */ 23 | @Test 24 | public void prioritizesProvidedApplicationInfo() { 25 | 26 | Application application = ApplicationProvider.getApplicationContext(); 27 | EnvironmentReporterBuilder builder1 = new EnvironmentReporterBuilder(); 28 | builder1.enableCollectionFromPlatform(application); 29 | IEnvironmentReporter reporter1 = builder1.build(); 30 | ApplicationInfo reporter1Output = reporter1.getApplicationInfo(); 31 | 32 | EnvironmentReporterBuilder builder2 = new EnvironmentReporterBuilder(); 33 | ApplicationInfo manualInfoInput = new ApplicationInfoBuilder().applicationId("manualAppID").createApplicationInfo(); 34 | builder2.setApplicationInfo(manualInfoInput); 35 | builder2.enableCollectionFromPlatform(application); 36 | IEnvironmentReporter reporter2 = builder2.build(); 37 | ApplicationInfo reporter2Output = reporter2.getApplicationInfo(); 38 | 39 | Assert.assertNotEquals(reporter1Output.getApplicationId(), reporter2Output.getApplicationId()); 40 | Assert.assertEquals(manualInfoInput.getApplicationId(), reporter2Output.getApplicationId()); 41 | } 42 | 43 | @Test 44 | public void defaultsToSDKValues() { 45 | IEnvironmentReporter reporter = new EnvironmentReporterBuilder().build(); 46 | Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationId()); 47 | Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); 48 | Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); 49 | Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); 50 | } 51 | 52 | @Test 53 | public void fallBackWhenIDMissing() { 54 | EnvironmentReporterBuilder builder = new EnvironmentReporterBuilder(); 55 | ApplicationInfo manualInfoInput = new ApplicationInfoBuilder().applicationName("imNotAnID!").createApplicationInfo(); 56 | builder.setApplicationInfo(manualInfoInput); 57 | IEnvironmentReporter reporter = builder.build(); 58 | Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationId()); 59 | Assert.assertEquals(LDPackageConsts.SDK_NAME, reporter.getApplicationInfo().getApplicationName()); 60 | Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersion()); 61 | Assert.assertEquals(BuildConfig.VERSION_NAME, reporter.getApplicationInfo().getApplicationVersionName()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientLoggingTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static org.hamcrest.CoreMatchers.containsString; 4 | import static org.hamcrest.MatcherAssert.assertThat; 5 | import static org.junit.Assert.assertNotEquals; 6 | 7 | import android.app.Application; 8 | 9 | import androidx.test.core.app.ApplicationProvider; 10 | import androidx.test.ext.junit.runners.AndroidJUnit4; 11 | 12 | import com.launchdarkly.logging.LDLogLevel; 13 | import com.launchdarkly.logging.LogCapture; 14 | import com.launchdarkly.logging.Logs; 15 | import com.launchdarkly.sdk.LDContext; 16 | import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; 17 | 18 | import org.junit.Before; 19 | import org.junit.Test; 20 | import org.junit.runner.RunWith; 21 | 22 | @RunWith(AndroidJUnit4.class) 23 | public class LDClientLoggingTest { 24 | 25 | private static final String mobileKey = "test-mobile-key"; 26 | private Application application; 27 | private LDContext ldContext; 28 | 29 | @Before 30 | public void setUp() { 31 | application = ApplicationProvider.getApplicationContext(); 32 | ldContext = LDContext.create("key"); 33 | } 34 | 35 | @Test 36 | public void customLogAdapterWithDefaultLevel() throws Exception { 37 | LogCapture logCapture = Logs.capture(); 38 | LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) 39 | .logAdapter(logCapture).build(); 40 | try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) { 41 | for (LogCapture.Message m: logCapture.getMessages()) { 42 | assertNotEquals(LDLogLevel.DEBUG, m.getLevel()); 43 | } 44 | LogCapture.Message m1 = logCapture.requireMessage(LDLogLevel.INFO, 2000); 45 | assertThat(m1.getText(), containsString("Initializing Client")); 46 | } 47 | } 48 | 49 | @Test 50 | public void customLogAdapterWithDebugLevel() throws Exception { 51 | LogCapture logCapture = Logs.capture(); 52 | LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey(mobileKey).offline(true) 53 | .logAdapter(logCapture).logLevel(LDLogLevel.DEBUG).build(); 54 | try (LDClient ldClient = LDClient.init(application, config, ldContext, 1)) { 55 | logCapture.requireMessage(LDLogLevel.DEBUG, 2000); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestActivity.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | 6 | public class TestActivity extends Activity { 7 | 8 | @Override 9 | public void onCreate(Bundle savedInstanceState) { 10 | super.onCreate(savedInstanceState); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestDataWithLDClientTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static org.junit.Assert.assertEquals; 4 | import static org.junit.Assert.assertFalse; 5 | import static org.junit.Assert.assertTrue; 6 | 7 | import android.app.Application; 8 | 9 | import androidx.test.core.app.ApplicationProvider; 10 | import androidx.test.ext.junit.runners.AndroidJUnit4; 11 | 12 | import com.launchdarkly.sdk.LDContext; 13 | import com.launchdarkly.sdk.LDValue; 14 | import com.launchdarkly.sdk.android.LDConfig.Builder.AutoEnvAttributes; 15 | import com.launchdarkly.sdk.android.integrations.TestData; 16 | 17 | import org.junit.Before; 18 | import org.junit.Test; 19 | import org.junit.runner.RunWith; 20 | 21 | @RunWith(AndroidJUnit4.class) 22 | public class TestDataWithLDClientTest { 23 | private final TestData td = TestData.dataSource(); 24 | private final LDConfig config; 25 | private final LDContext context = LDContext.create("userkey"); 26 | 27 | private Application application; 28 | 29 | public TestDataWithLDClientTest() { 30 | config = new LDConfig.Builder(AutoEnvAttributes.Disabled).mobileKey("mobile-key") 31 | .dataSource(td) 32 | .events(Components.noEvents()) 33 | .build(); 34 | } 35 | 36 | @Before 37 | public void setUp() { 38 | application = ApplicationProvider.getApplicationContext(); 39 | } 40 | 41 | private LDClient makeClient() throws Exception { 42 | return LDClient.init(application, config, context, 5); 43 | } 44 | 45 | @Test 46 | public void initializesWithEmptyData() throws Exception { 47 | try (LDClient client = makeClient()) { 48 | assertTrue(client.isInitialized()); 49 | } 50 | } 51 | 52 | @Test 53 | public void initializesWithFlag() throws Exception { 54 | td.update(td.flag("flag").variation(true)); 55 | 56 | try (LDClient client = makeClient()) { 57 | assertTrue(client.boolVariation("flag", false)); 58 | } 59 | } 60 | 61 | @Test 62 | public void updateFlag() throws Exception { 63 | try (LDClient client = makeClient()) { 64 | assertFalse(client.boolVariation("flag", false)); 65 | 66 | td.update(td.flag("flag").variation(true)); 67 | 68 | assertTrue(client.boolVariation("flag", false)); 69 | } 70 | } 71 | 72 | @Test 73 | public void canSetValuePerContext() throws Exception { 74 | td.update(td.flag("flag") 75 | .variations(LDValue.of("red"), LDValue.of("green"), LDValue.of("blue")) 76 | .variation(0) 77 | .variationValueFunc(c -> c.getValue("favoriteColor")) 78 | ); 79 | LDContext context1 = LDContext.create("user1"); 80 | LDContext context2 = LDContext.builder("user2").set("favoriteColor", "green").build(); 81 | LDContext context3 = LDContext.builder("user3").set("favoriteColor", "blue").build(); 82 | 83 | try (LDClient client = LDClient.init(application, config, context1, 5);) { 84 | assertEquals("red", client.stringVariation("flag", "")); 85 | 86 | client.identify(context2).get(); 87 | assertEquals("green", client.stringVariation("flag", "")); 88 | 89 | client.identify(context3).get(); 90 | assertEquals("blue", client.stringVariation("flag", "")); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AndroidTaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import android.app.Application; 4 | import android.os.Handler; 5 | import android.os.Looper; 6 | 7 | import com.launchdarkly.logging.LDLogger; 8 | 9 | import java.util.concurrent.Executors; 10 | import java.util.concurrent.ScheduledExecutorService; 11 | import java.util.concurrent.ScheduledFuture; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | /** 15 | * Standard implementation of {@link TaskExecutor} for use in the Android environment. Besides 16 | * enforcing correct thread usage, this class also ensures that any unchecked exceptions thrown by 17 | * asynchronous tasks are caught and logged. 18 | */ 19 | final class AndroidTaskExecutor implements TaskExecutor { 20 | private final Application application; 21 | private final Handler handler; 22 | private final LDLogger logger; 23 | private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 24 | 25 | AndroidTaskExecutor(Application application, LDLogger logger) { 26 | this.application = application; 27 | this.handler = new Handler(Looper.getMainLooper()); 28 | this.logger = logger; 29 | } 30 | 31 | @Override 32 | public void executeOnMainThread(Runnable action) { 33 | if (Looper.myLooper() == Looper.getMainLooper()) { 34 | callActionWithErrorHandling(action); 35 | } else { 36 | handler.post(wrapActionWithErrorHandling(action)); 37 | } 38 | } 39 | 40 | @Override 41 | public ScheduledFuture scheduleTask(Runnable action, long delayMillis) { 42 | return executor.schedule(wrapActionWithErrorHandling(action), delayMillis, TimeUnit.MILLISECONDS); 43 | } 44 | 45 | @Override 46 | public ScheduledFuture startRepeatingTask(Runnable action, long initialDelayMillis, long intervalMillis) { 47 | return executor.scheduleAtFixedRate(wrapActionWithErrorHandling(action), 48 | initialDelayMillis, intervalMillis, TimeUnit.MILLISECONDS); 49 | } 50 | 51 | @Override 52 | public void close() { 53 | executor.shutdownNow(); 54 | } 55 | 56 | private Runnable wrapActionWithErrorHandling(Runnable action) { 57 | return new Runnable() { 58 | @Override 59 | public void run() { 60 | callActionWithErrorHandling(action); 61 | } 62 | }; 63 | } 64 | 65 | private void callActionWithErrorHandling(Runnable action) { 66 | try { 67 | if (action != null) { 68 | action.run(); 69 | } 70 | } catch (RuntimeException e) { 71 | LDUtil.logExceptionAtErrorLevel(logger, e, "Unexpected exception from asynchronous task"); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/AnonymousKeyContextModifier.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.logging.LDLogger; 6 | import com.launchdarkly.sdk.ContextMultiBuilder; 7 | import com.launchdarkly.sdk.LDContext; 8 | 9 | /** 10 | * A {@link IContextModifier} that will set the key of anonymous contexts to a randomly 11 | * generated one. Generated keys are persisted and consistent for a given context kind 12 | * across calls to {@link #modifyContext(LDContext)}. 13 | */ 14 | final class AnonymousKeyContextModifier implements IContextModifier { 15 | 16 | @NonNull private final PersistentDataStoreWrapper persistentData; 17 | private final boolean generateAnonymousKeys; 18 | 19 | /** 20 | * @param persistentData that will be used for storing/retrieving keys 21 | * @param generateAnonymousKeys controls whether generated keys will be applied 22 | */ 23 | public AnonymousKeyContextModifier( 24 | @NonNull PersistentDataStoreWrapper persistentData, 25 | boolean generateAnonymousKeys 26 | ) { 27 | this.persistentData = persistentData; 28 | this.generateAnonymousKeys = generateAnonymousKeys; 29 | } 30 | 31 | public LDContext modifyContext(LDContext context) { 32 | if (!generateAnonymousKeys) { 33 | return context; 34 | } 35 | if (context.isMultiple()) { 36 | boolean hasAnyAnon = false; 37 | for (int i = 0; i < context.getIndividualContextCount(); i++) { 38 | if (context.getIndividualContext(i).isAnonymous()) { 39 | hasAnyAnon = true; 40 | break; 41 | } 42 | } 43 | if (hasAnyAnon) { 44 | ContextMultiBuilder builder = LDContext.multiBuilder(); 45 | for (int i = 0; i < context.getIndividualContextCount(); i++) { 46 | LDContext c = context.getIndividualContext(i); 47 | builder.add(c.isAnonymous() ? singleKindContextWithGeneratedKey(c) : c); 48 | } 49 | return builder.build(); 50 | } 51 | } else if (context.isAnonymous()) { 52 | return singleKindContextWithGeneratedKey(context); 53 | } 54 | return context; 55 | } 56 | 57 | private LDContext singleKindContextWithGeneratedKey(LDContext context) { 58 | return LDContext.builderFromContext(context) 59 | .key(persistentData.getOrGenerateContextKey(context.getKind())) 60 | .build(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionInformation.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | /** 4 | * Provides various information about a current or previous connection. 5 | */ 6 | public interface ConnectionInformation { 7 | 8 | /** 9 | * Enumerated type defining the possible values of {@link ConnectionInformation#getConnectionMode()}. 10 | */ 11 | enum ConnectionMode { 12 | /** 13 | * The SDK is either connected to the flag stream, or is actively attempting to acquire a connection. 14 | */ 15 | STREAMING(true), 16 | 17 | /** 18 | * The SDK is in foreground polling mode because it was configured with streaming disabled. 19 | */ 20 | POLLING(true), 21 | 22 | /** 23 | * The SDK has detected the application is in the background and has transitioned to battery-saving background 24 | * polling. 25 | */ 26 | BACKGROUND_POLLING(true), 27 | 28 | /** 29 | * The SDK was configured with background polling disabled. The SDK has detected the application is in the 30 | * background and is not attempting to update the flag cache. 31 | */ 32 | BACKGROUND_DISABLED(false), 33 | 34 | /** 35 | * The SDK has detected that the mobile device does not have an active network connection. It has ceased flag 36 | * update attempts until the network status changes. 37 | */ 38 | OFFLINE(false), 39 | 40 | /** 41 | * The SDK has been explicitly set offline, either in the initial configuration, by 42 | * {@link LDClient#setOffline()}, or as a result of failed authentication to LaunchDarkly. The SDK will stay 43 | * offline unless {@link LDClient#setOnline()} is called. 44 | */ 45 | SET_OFFLINE(false), 46 | 47 | /** 48 | * The shutdown state indicates the SDK has been permanently shutdown as a result of a call to close(). 49 | */ 50 | SHUTDOWN(false); 51 | 52 | private boolean connectionActive; 53 | 54 | ConnectionMode(boolean connectionActive) { 55 | this.connectionActive = connectionActive; 56 | } 57 | 58 | /** 59 | * @return true if connection is active, false otherwise 60 | */ 61 | boolean isConnectionActive() { 62 | return connectionActive; 63 | } 64 | } 65 | 66 | /** 67 | * @return the {@link ConnectionMode} 68 | */ 69 | ConnectionMode getConnectionMode(); 70 | 71 | /** 72 | * @return the last {@link LDFailure} 73 | */ 74 | LDFailure getLastFailure(); 75 | 76 | /** 77 | * @return millis since epoch when the last successful connection occurred 78 | */ 79 | Long getLastSuccessfulConnection(); 80 | 81 | /** 82 | * @return millis since epoch when the last connection connection failure occurred 83 | */ 84 | Long getLastFailedConnection(); 85 | } 86 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionInformationState.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | class ConnectionInformationState implements ConnectionInformation { 4 | private ConnectionMode connectionMode; 5 | private LDFailure lastFailure; 6 | private Long lastSuccessfulConnection; 7 | private Long lastFailedConnection; 8 | 9 | public ConnectionMode getConnectionMode() { 10 | return connectionMode; 11 | } 12 | 13 | void setConnectionMode(ConnectionMode connectionMode) { 14 | this.connectionMode = connectionMode; 15 | } 16 | 17 | public LDFailure getLastFailure() { 18 | return lastFailure; 19 | } 20 | 21 | void setLastFailure(LDFailure lastFailure) { 22 | this.lastFailure = lastFailure; 23 | } 24 | 25 | public Long getLastSuccessfulConnection() { 26 | return lastSuccessfulConnection; 27 | } 28 | 29 | void setLastSuccessfulConnection(Long lastSuccessfulConnection) { 30 | this.lastSuccessfulConnection = lastSuccessfulConnection; 31 | } 32 | 33 | public Long getLastFailedConnection() { 34 | return lastFailedConnection; 35 | } 36 | 37 | void setLastFailedConnection(Long lastFailedConnection) { 38 | this.lastFailedConnection = lastFailedConnection; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Debounce.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.util.concurrent.Callable; 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | 7 | class Debounce { 8 | private volatile Callable pending; 9 | private volatile Callable inFlight = null; 10 | private final ExecutorService service = Executors.newSingleThreadExecutor(); 11 | 12 | synchronized void call(Callable task) { 13 | pending = task; 14 | 15 | schedulePending(); 16 | } 17 | 18 | private synchronized void schedulePending() { 19 | if (pending == null) { 20 | return; 21 | } 22 | 23 | if (inFlight == null) { 24 | inFlight = pending; 25 | service.submit(() -> { 26 | try { 27 | inFlight.call(); 28 | } finally { 29 | inFlight = null; 30 | schedulePending(); 31 | } 32 | return null; 33 | }); 34 | pending = null; 35 | } 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EnvironmentData.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.google.gson.reflect.TypeToken; 6 | import com.launchdarkly.sdk.android.DataModel.Flag; 7 | import com.launchdarkly.sdk.internal.GsonHelpers; 8 | import com.launchdarkly.sdk.json.SerializationException; 9 | 10 | import java.lang.reflect.Type; 11 | import java.util.Collection; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * An immutable set of flag data. 17 | */ 18 | final class EnvironmentData { 19 | static final Type FLAGS_MAP_TYPE = 20 | new TypeToken>() {}.getType(); 21 | 22 | @NonNull 23 | private final Map flags; 24 | 25 | public EnvironmentData() { 26 | this(new HashMap<>()); 27 | } 28 | 29 | private EnvironmentData(Map flags) { 30 | this.flags = flags == null ? new HashMap<>() : flags; 31 | } 32 | 33 | public static EnvironmentData copyingFlagsMap(Map flags) { 34 | return new EnvironmentData(flags == null ? null : new HashMap<>(flags)); 35 | } 36 | 37 | public static EnvironmentData usingExistingFlagsMap(Map flags) { 38 | return new EnvironmentData(flags); 39 | } 40 | 41 | public Flag getFlag(String key) { 42 | return flags.get(key); 43 | } 44 | 45 | public Map getAll() { 46 | return new HashMap<>(flags); 47 | } 48 | 49 | public Collection values() { 50 | return flags.values(); 51 | } 52 | 53 | public EnvironmentData withFlagUpdatedOrAdded(Flag flag) { 54 | if (flag == null) { 55 | return this; 56 | } 57 | Map newFlags = new HashMap<>(flags); 58 | newFlags.put(flag.getKey(), flag); 59 | return new EnvironmentData(newFlags); 60 | } 61 | 62 | public EnvironmentData withFlagRemoved(String key) { 63 | if (key == null || !flags.containsKey(key)) { 64 | return this; 65 | } 66 | Map newFlags = new HashMap<>(flags); 67 | newFlags.remove(key); 68 | return new EnvironmentData(newFlags); 69 | } 70 | 71 | public static EnvironmentData fromJson(String json) throws SerializationException { 72 | Map dataMap; 73 | try { 74 | dataMap = GsonHelpers.gsonInstance().fromJson(json, FLAGS_MAP_TYPE); 75 | } catch (Exception e) { // Gson throws various kinds of parsing exceptions that have no common base class 76 | throw new SerializationException(e); 77 | } 78 | // Normalize the data set to ensure that the flag keys are present not only as map keys, 79 | // but also in each Flag object. That is normally the case in data sent by LD, even though 80 | // it's redundant, but if for any reason it isn't we can transparently fix it. 81 | for (Map.Entry entry: dataMap.entrySet()) { 82 | Flag f = entry.getValue(); 83 | if (f.getKey() == null) { 84 | f = new Flag(entry.getKey(), f.getValue(), f.getVersion(), f.getFlagVersion(), 85 | f.getVariation(), f.isTrackEvents(), f.isTrackReason(), f.getDebugEventsUntilDate(), 86 | f.getReason(), f.getPrerequisites()); 87 | entry.setValue(f); 88 | } 89 | } 90 | return new EnvironmentData(dataMap); 91 | } 92 | 93 | public String toJson() { 94 | return GsonHelpers.gsonInstance().toJson(flags); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FeatureFetcher.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | import com.launchdarkly.sdk.android.subsystems.Callback; 5 | 6 | import java.io.Closeable; 7 | 8 | interface FeatureFetcher extends Closeable { 9 | void fetch(LDContext context, final Callback callback); 10 | } 11 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FeatureFlagChangeListener.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | /** 4 | * Callback interface used for listening to changes to a feature flag. 5 | * 6 | * @see LDClientInterface#registerFeatureFlagListener(String, FeatureFlagChangeListener) 7 | */ 8 | @FunctionalInterface 9 | public interface FeatureFlagChangeListener { 10 | /** 11 | * The SDK calls this method when a feature flag value has changed for the current evaluation context. 12 | *

13 | * To obtain the new value, call one of the client methods such as {@link LDClientInterface#boolVariation(String, boolean)}. 14 | * 15 | * @param flagKey the feature flag key 16 | */ 17 | void onFeatureFlagChange(String flagKey); 18 | } 19 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/IContextModifier.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | 5 | /** 6 | * Modifies contexts when invoked. 7 | */ 8 | public interface IContextModifier { 9 | 10 | /** 11 | * Modifies the provided context and returns a resulting context. May result in no changes at 12 | * the discretion of the implementation. 13 | * 14 | * @param context to be modified 15 | * @return another context that is the result of modification 16 | */ 17 | LDContext modifyContext(LDContext context); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDAllFlagsListener.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * Callback interface used for listening to changes to the flag store. 7 | */ 8 | @FunctionalInterface 9 | public interface LDAllFlagsListener { 10 | 11 | /** 12 | * Called by the SDK whenever it receives an update for the stored flag values of the current context. 13 | * 14 | * @param flagKey A list of flag keys which were created, updated, or deleted as part of the update. 15 | * This list may be empty if the update resulted in no changed flag values. 16 | */ 17 | void onChange(List flagKey); 18 | 19 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFailure.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.google.gson.annotations.JsonAdapter; 6 | 7 | /** 8 | * Container class representing a communication failure with LaunchDarkly servers. 9 | */ 10 | @JsonAdapter(LDFailureSerialization.class) 11 | public class LDFailure extends LaunchDarklyException { 12 | 13 | /** 14 | * Enumerated type defining the possible values of {@link LDFailure#getFailureType()}. 15 | */ 16 | public enum FailureType { 17 | /** 18 | * A response body received either through polling or streaming was unable to be parsed. 19 | */ 20 | INVALID_RESPONSE_BODY, 21 | 22 | /** 23 | * A network request for polling, or the EventSource stream reported a failure. 24 | */ 25 | NETWORK_FAILURE, 26 | 27 | /** 28 | * An event was received through the stream with an unknown event key. This could indicate a newer SDK is 29 | * available if new event kinds have become available through the flag stream since the SDK's release. 30 | */ 31 | UNEXPECTED_STREAM_ELEMENT_TYPE, 32 | 33 | /** 34 | * This indicates the LDFailure is an instance of LDInvalidResponseCodeFailure. 35 | */ 36 | UNEXPECTED_RESPONSE_CODE, 37 | 38 | /** 39 | * Some other issue occurred. 40 | */ 41 | UNKNOWN_ERROR 42 | } 43 | 44 | /** 45 | * The failure type 46 | */ 47 | @NonNull 48 | private final FailureType failureType; 49 | 50 | /** 51 | * @param message the message 52 | * @param failureType the failure type 53 | */ 54 | public LDFailure(String message, @NonNull FailureType failureType) { 55 | super(message); 56 | this.failureType = failureType; 57 | } 58 | 59 | /** 60 | * @param message the message 61 | * @param cause the cause of the failure 62 | * @param failureType the failure type 63 | */ 64 | public LDFailure(String message, Throwable cause, @NonNull FailureType failureType) { 65 | super(message, cause); 66 | this.failureType = failureType; 67 | } 68 | 69 | /** 70 | * @return the failure type 71 | */ 72 | @NonNull 73 | public FailureType getFailureType() { 74 | return failureType; 75 | } 76 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDFailureSerialization.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.google.gson.JsonDeserializationContext; 4 | import com.google.gson.JsonDeserializer; 5 | import com.google.gson.JsonElement; 6 | import com.google.gson.JsonObject; 7 | import com.google.gson.JsonParseException; 8 | import com.google.gson.JsonSerializationContext; 9 | import com.google.gson.JsonSerializer; 10 | 11 | import java.lang.reflect.Type; 12 | 13 | class LDFailureSerialization implements JsonSerializer, JsonDeserializer { 14 | @Override 15 | public LDFailure deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 16 | JsonObject in = json.getAsJsonObject(); 17 | LDFailure.FailureType failureType = context.deserialize(in.get("failureType"), LDFailure.FailureType.class); 18 | String message = in.getAsJsonPrimitive("message").getAsString(); 19 | if (failureType == LDFailure.FailureType.UNEXPECTED_RESPONSE_CODE) { 20 | int responseCode = in.getAsJsonPrimitive("responseCode").getAsInt(); 21 | boolean retryable = in.getAsJsonPrimitive("retryable").getAsBoolean(); 22 | return new LDInvalidResponseCodeFailure(message, responseCode, retryable); 23 | } else { 24 | return new LDFailure(message, failureType); 25 | } 26 | } 27 | 28 | @Override 29 | public JsonElement serialize(LDFailure src, Type typeOfSrc, JsonSerializationContext context) { 30 | if (src == null) { 31 | return null; 32 | } 33 | JsonObject jsonObject = new JsonObject(); 34 | jsonObject.add("failureType", context.serialize(src.getFailureType())); 35 | jsonObject.addProperty("message", src.getMessage()); 36 | if (src instanceof LDInvalidResponseCodeFailure) { 37 | LDInvalidResponseCodeFailure fail = (LDInvalidResponseCodeFailure) src; 38 | jsonObject.addProperty("responseCode", fail.getResponseCode()); 39 | jsonObject.addProperty("retryable", fail.isRetryable()); 40 | } 41 | return jsonObject; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDHeaderUpdater.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * An interface to provide the SDK with a function used to modify HTTP headers before each request 7 | * to the LaunchDarkly service. 8 | */ 9 | public interface LDHeaderUpdater { 10 | /** 11 | * An application provided method for dynamic configuration of HTTP headers. 12 | * 13 | * @param headers The unmodified headers the SDK prepared for the request 14 | */ 15 | void updateHeaders(Map headers); 16 | } 17 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDInvalidResponseCodeFailure.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.google.gson.annotations.JsonAdapter; 4 | 5 | /** 6 | * Container class representing a communication failure with LaunchDarkly servers in which the response was unexpected. 7 | */ 8 | @JsonAdapter(LDFailureSerialization.class) 9 | public class LDInvalidResponseCodeFailure extends LDFailure { 10 | 11 | /** 12 | * The response code 13 | */ 14 | private final int responseCode; 15 | 16 | /** 17 | * Whether or not the failure may be fixed by retrying 18 | */ 19 | private final boolean retryable; 20 | 21 | /** 22 | * @param message the message 23 | * @param responseCode the response code 24 | * @param retryable whether or not retrying may resolve the issue 25 | */ 26 | public LDInvalidResponseCodeFailure(String message, int responseCode, boolean retryable) { 27 | super(message, FailureType.UNEXPECTED_RESPONSE_CODE); 28 | this.responseCode = responseCode; 29 | this.retryable = retryable; 30 | } 31 | 32 | /** 33 | * @param message the message 34 | * @param cause the cause of the failure 35 | * @param responseCode the response code 36 | * @param retryable whether or not retrying may resolve the issue 37 | */ 38 | public LDInvalidResponseCodeFailure(String message, Throwable cause, int responseCode, boolean retryable) { 39 | super(message, cause, FailureType.UNEXPECTED_RESPONSE_CODE); 40 | this.responseCode = responseCode; 41 | this.retryable = retryable; 42 | } 43 | 44 | /** 45 | * @return true if retrying may resolve the issue 46 | */ 47 | public boolean isRetryable() { 48 | return retryable; 49 | } 50 | 51 | /** 52 | * @return the response code 53 | */ 54 | public int getResponseCode() { 55 | return responseCode; 56 | } 57 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDPackageConsts.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | /** 4 | * Constants related to the SDK package 5 | */ 6 | public class LDPackageConsts { 7 | 8 | /** 9 | * Name of the SDK 10 | */ 11 | public static final String SDK_NAME = "android-client-sdk"; 12 | 13 | /** 14 | * Name of the platform this SDK is primarily for 15 | */ 16 | public static final String SDK_PLATFORM_NAME = "Android"; 17 | 18 | /** 19 | * Name that will be used for identifying this SDK when using a network client. An example would be the 20 | * user agent in HTTP. 21 | */ 22 | public static final String SDK_CLIENT_NAME = "AndroidClient"; 23 | 24 | /** 25 | * Name the logger will use to identify this SDK. 26 | */ 27 | public static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; 28 | } 29 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDStatusListener.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | /** 4 | * Listener for various SDK state changes. 5 | */ 6 | public interface LDStatusListener { 7 | 8 | /** 9 | * Invoked when the connection mode changes 10 | * @param connectionInformation the connection information that gives details about the connection 11 | */ 12 | void onConnectionModeChanged(ConnectionInformation connectionInformation); 13 | 14 | /** 15 | * Invoked when an internal issue results in a failure to connect to LaunchDarkly 16 | * @param ldFailure the failure 17 | */ 18 | void onInternalFailure(LDFailure ldFailure); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LaunchDarklyException.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | /** 4 | * Exception class that can be thrown by LaunchDarkly client methods. 5 | */ 6 | public class LaunchDarklyException extends Exception { 7 | 8 | /** 9 | * @param message for the exception 10 | */ 11 | public LaunchDarklyException(String message) { 12 | super(message); 13 | } 14 | 15 | LaunchDarklyException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/NoOpContextModifier.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | 5 | /** 6 | * Context modifier that does nothing to the context. 7 | */ 8 | public class NoOpContextModifier implements IContextModifier { 9 | 10 | @Override 11 | public LDContext modifyContext(LDContext context) { 12 | return context; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PlatformState.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.io.Closeable; 4 | import java.io.File; 5 | 6 | interface PlatformState extends Closeable { 7 | interface ConnectivityChangeListener { 8 | void onConnectivityChanged(boolean networkAvailable); 9 | } 10 | 11 | interface ForegroundChangeListener { 12 | void onForegroundChanged(boolean foreground); 13 | } 14 | 15 | /** 16 | * Returns true if (as far as the OS knows) the network should be working. 17 | * @return true if the network should be available 18 | */ 19 | boolean isNetworkAvailable(); 20 | 21 | /** 22 | * Registers a listener to be called if the state of {@link #isNetworkAvailable()}} changes. 23 | * @param listener a listener 24 | */ 25 | void addConnectivityChangeListener(ConnectivityChangeListener listener); 26 | 27 | /** 28 | * Undoes the effect of {@link #addConnectivityChangeListener(ConnectivityChangeListener)}. Has 29 | * no effect if no such listener is registered. 30 | * @param listener a listener 31 | */ 32 | void removeConnectivityChangeListener(ConnectivityChangeListener listener); 33 | 34 | /** 35 | * Returns true if we believe the application is in the foreground, false if we believe it is in 36 | * the background. 37 | * @return true if in the foreground 38 | */ 39 | boolean isForeground(); 40 | 41 | /** 42 | * Registers a listener to be called if the state of {@link #isForeground()} changes. 43 | * @param listener a listener 44 | */ 45 | void addForegroundChangeListener(ForegroundChangeListener listener); 46 | 47 | /** 48 | * Undoes the effect of {@link #addForegroundChangeListener(ForegroundChangeListener)}. 49 | * @param listener 50 | */ 51 | void removeForegroundChangeListener(ForegroundChangeListener listener); 52 | 53 | /** 54 | * Returns the preferred filesystem location for cache files. 55 | * @return a directory path 56 | */ 57 | File getCacheDir(); 58 | } 59 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.logging.LDLogger; 4 | 5 | import java.net.URI; 6 | 7 | abstract class StandardEndpoints { 8 | private StandardEndpoints() {} 9 | 10 | static final URI DEFAULT_STREAMING_BASE_URI = URI.create("https://clientstream.launchdarkly.com"); 11 | static final URI DEFAULT_POLLING_BASE_URI = URI.create("https://clientsdk.launchdarkly.com"); 12 | static final URI DEFAULT_EVENTS_BASE_URI = URI.create("https://mobile.launchdarkly.com"); 13 | 14 | static final String STREAMING_REQUEST_BASE_PATH = "/meval"; 15 | static final String POLLING_REQUEST_GET_BASE_PATH = "/msdk/evalx/contexts"; 16 | static final String POLLING_REQUEST_REPORT_BASE_PATH = "/msdk/evalx/context"; 17 | static final String ANALYTICS_EVENTS_REQUEST_PATH = "/mobile/events/bulk"; 18 | static final String DIAGNOSTIC_EVENTS_REQUEST_PATH = "/mobile/events/diagnostic"; 19 | 20 | /** 21 | * Internal method to decide which URI a given component should connect to. 22 | *

23 | * Always returns some URI, falling back on the default if necessary, but logs a warning if we detect that the application 24 | * set some custom endpoints but not this one. 25 | * 26 | * @param serviceEndpointsValue the value set in ServiceEndpoints (this is either the default URI, a custom URI, or null) 27 | * @param defaultValue the constant default URI value defined in StandardEndpoints 28 | * @param description a human-readable string for the type of endpoint being selected, for logging purposes 29 | * @param logger the logger to which we should print the warning, if needed 30 | * @return the base URI we should connect to 31 | */ 32 | static URI selectBaseUri(URI serviceEndpointsValue, URI defaultValue, String description, LDLogger logger) { 33 | if (serviceEndpointsValue != null) { 34 | return serviceEndpointsValue; 35 | } 36 | logger.warn("You have set custom ServiceEndpoints without specifying the {} base URI; connections may not work properly", description); 37 | return defaultValue; 38 | } 39 | 40 | /** 41 | * Internal method to determine whether a given base URI was set to a custom value or not. 42 | *

43 | * This boolean value is only used for our diagnostic events. We only check if the value 44 | * differs from the default; if the base URI was "overridden" in configuration, but 45 | * happens to be equal to the default URI, we don't count that as custom 46 | * for the purposes of this diagnostic. 47 | * 48 | * @param serviceEndpointsValue the value set in ServiceEndpoints 49 | * @param defaultValue the constant default URI value defined in StandardEndpoints 50 | * @return true iff the base URI was customized 51 | */ 52 | static boolean isCustomBaseUri(URI serviceEndpointsValue, URI defaultValue) { 53 | return serviceEndpointsValue != null && !serviceEndpointsValue.equals(defaultValue); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/TaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.io.Closeable; 4 | import java.util.concurrent.ScheduledFuture; 5 | 6 | /** 7 | * Internal abstraction for standardizing how asynchronous tasks are executed. 8 | */ 9 | interface TaskExecutor extends Closeable { 10 | /** 11 | * Causes an action to be performed on the main thread. We use this when we are calling 12 | * application-provided listeners. 13 | *

14 | * If we are already on the main thread, the action is called synchronously. Otherwise, it is 15 | * scheduled to be run asynchronously on the main thread. 16 | * 17 | * @param action the action to execute 18 | */ 19 | void executeOnMainThread(Runnable action); 20 | 21 | /** 22 | * Schedules an action to be done asynchronously by a worker. It will not be done on the main 23 | * thread. There are no guarantees as to ordering with other tasks. 24 | * 25 | * @param action the action to execute 26 | * @param delayMillis minimum milliseconds to wait before executing 27 | * @return a ScheduledFuture that can be used to cancel the task 28 | */ 29 | ScheduledFuture scheduleTask(Runnable action, long delayMillis); 30 | 31 | /** 32 | * Schedules an action to be run repeatedly at intervals. It will not be done on the main thread. 33 | * 34 | * @param action the action to execute at each interval 35 | * @param initialDelayMillis milliseconds to wait before the first execution 36 | * @param intervalMillis milliseconds between executions 37 | * @return a ScheduledFuture that can be used to cancel the task 38 | */ 39 | ScheduledFuture startRepeatingTask(Runnable action, long initialDelayMillis, long intervalMillis); 40 | } 41 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/ApplicationInfoEnvironmentReporter.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.env; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 6 | 7 | /** 8 | * An {@link IEnvironmentReporter} that reports the provided {@link ApplicationInfo} for {@link #getApplicationInfo()} 9 | * and defers all other attributes to the next reporter in the chain. 10 | */ 11 | class ApplicationInfoEnvironmentReporter extends EnvironmentReporterChainBase implements IEnvironmentReporter { 12 | 13 | private ApplicationInfo applicationInfo; 14 | 15 | public ApplicationInfoEnvironmentReporter(ApplicationInfo applicationInfo) { 16 | this.applicationInfo = applicationInfo; 17 | } 18 | 19 | @NonNull 20 | @Override 21 | public ApplicationInfo getApplicationInfo() { 22 | // defer to super if required property applicationID is missing 23 | if (applicationInfo.getApplicationId() == null) { 24 | return super.getApplicationInfo(); 25 | } 26 | return applicationInfo; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterBuilder.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.env; 2 | 3 | import android.app.Application; 4 | 5 | import androidx.annotation.Nullable; 6 | 7 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * Builder for making an {@link IEnvironmentReporter} with various options. 14 | */ 15 | public class EnvironmentReporterBuilder { 16 | 17 | @Nullable 18 | private Application application; 19 | 20 | @Nullable 21 | private ApplicationInfo applicationInfo; 22 | 23 | /** 24 | * Sets the application info that this environment reporter will report when asked in the future, 25 | * overriding the automatically sourced {@link ApplicationInfo} 26 | * 27 | * @param applicationInfo to report. 28 | */ 29 | public void setApplicationInfo(ApplicationInfo applicationInfo) { 30 | this.applicationInfo = applicationInfo; 31 | } 32 | 33 | /** 34 | * Enables automatically collecting attributes from the platform. 35 | * 36 | * @param application reference for platform calls 37 | */ 38 | public void enableCollectionFromPlatform(Application application) { 39 | this.application = application; 40 | } 41 | 42 | /** 43 | * @return the {@link IEnvironmentReporter} 44 | */ 45 | public IEnvironmentReporter build() { 46 | /** 47 | * Create chain of responsibility with the following priority order 48 | * 1. {@link ApplicationInfoEnvironmentReporter} - holds customer override 49 | * 2. {@link AndroidEnvironmentReporter} - Android platform API next 50 | * 3. {@link SDKEnvironmentReporter} - Fallback is SDK constants 51 | */ 52 | List reporters = new ArrayList<>(); 53 | 54 | if (applicationInfo != null) { 55 | reporters.add(new ApplicationInfoEnvironmentReporter(applicationInfo)); 56 | } 57 | 58 | if (application != null) { 59 | reporters.add(new AndroidEnvironmentReporter(application)); 60 | } 61 | 62 | // always add fallback reporter 63 | reporters.add(new SDKEnvironmentReporter()); 64 | 65 | // build chain of responsibility by iterating on all but last element 66 | for (int i = 0; i < reporters.size() - 1; i++) { 67 | reporters.get(i).setNext(reporters.get(i + 1)); 68 | } 69 | 70 | // guaranteed non-empty since fallback reporter is always added 71 | return reporters.get(0); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/EnvironmentReporterChainBase.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.env; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 7 | 8 | /** 9 | * Base implementation for using {@link IEnvironmentReporter}s in a chain of responsibility pattern. 10 | */ 11 | class EnvironmentReporterChainBase implements IEnvironmentReporter { 12 | 13 | private static final String UNKNOWN = "unknown"; 14 | 15 | // the next reporter in the chain if there is one 16 | @Nullable 17 | protected EnvironmentReporterChainBase next; 18 | 19 | public void setNext(EnvironmentReporterChainBase next) { 20 | this.next = next; 21 | } 22 | 23 | @NonNull 24 | @Override 25 | public ApplicationInfo getApplicationInfo() { 26 | return next != null ? next.getApplicationInfo() : new ApplicationInfo(UNKNOWN, UNKNOWN, UNKNOWN, UNKNOWN); 27 | } 28 | 29 | @NonNull 30 | @Override 31 | public String getManufacturer() { 32 | return next != null ? next.getManufacturer() : UNKNOWN; 33 | } 34 | 35 | @NonNull 36 | @Override 37 | public String getModel() { 38 | return next != null ? next.getModel() : UNKNOWN; 39 | } 40 | 41 | @NonNull 42 | @Override 43 | public String getLocale() { 44 | return next != null ? next.getLocale() : UNKNOWN; 45 | } 46 | 47 | @NonNull 48 | @Override 49 | public String getOSFamily() { 50 | return next != null ? next.getOSFamily() : UNKNOWN; 51 | } 52 | 53 | @NonNull 54 | @Override 55 | public String getOSName() { 56 | return next != null ? next.getOSName() : UNKNOWN; 57 | } 58 | 59 | @NonNull 60 | @Override 61 | public String getOSVersion() { 62 | return next != null ? next.getOSVersion() : UNKNOWN; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/IEnvironmentReporter.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.env; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 6 | 7 | /** 8 | * Reports information about the software/hardware environment that the SDK is 9 | * executing within. 10 | */ 11 | public interface IEnvironmentReporter { 12 | 13 | /** 14 | * @return the {@link ApplicationInfo} for the application environment. 15 | */ 16 | @NonNull 17 | ApplicationInfo getApplicationInfo(); 18 | 19 | /** 20 | * @return the manufacturer of the device the application is running in 21 | */ 22 | @NonNull 23 | String getManufacturer(); 24 | 25 | /** 26 | * @return the model of the device the application is running in 27 | */ 28 | @NonNull 29 | String getModel(); 30 | 31 | /** 32 | * @return a BCP47 language tag representing the locale 33 | */ 34 | @NonNull 35 | String getLocale(); 36 | 37 | /** 38 | * @return the OS Family that this application is running in 39 | */ 40 | @NonNull 41 | String getOSFamily(); 42 | 43 | /** 44 | * @return the name of the OS that this application is running in 45 | */ 46 | @NonNull 47 | String getOSName(); 48 | 49 | /** 50 | * @return the version of the OS that this application is running in 51 | */ 52 | @NonNull 53 | String getOSVersion(); 54 | } 55 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/env/SDKEnvironmentReporter.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.env; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.sdk.android.BuildConfig; 6 | import com.launchdarkly.sdk.android.LDPackageConsts; 7 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 8 | 9 | /** 10 | * An {@link IEnvironmentReporter} that reports static SDK information for {@link #getApplicationInfo()} 11 | * and defers all other attributes to the next reporter in the chain. 12 | */ 13 | class SDKEnvironmentReporter extends EnvironmentReporterChainBase implements IEnvironmentReporter { 14 | 15 | @NonNull 16 | @Override 17 | public ApplicationInfo getApplicationInfo() { 18 | return new ApplicationInfo( 19 | LDPackageConsts.SDK_NAME, 20 | BuildConfig.VERSION_NAME, 21 | LDPackageConsts.SDK_NAME, 22 | BuildConfig.VERSION_NAME 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EvaluationSeriesContext.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | import com.launchdarkly.sdk.LDValue; 5 | 6 | import java.util.Map; 7 | import java.util.Objects; 8 | 9 | /** 10 | * Represents parameters associated with a feature flag evaluation. An instance of this class is provided to some 11 | * stages of series of a {@link Hook} implementation. For example, see {@link Hook#beforeEvaluation(EvaluationSeriesContext, Map)} 12 | */ 13 | public class EvaluationSeriesContext { 14 | 15 | /** 16 | * The variation method that was used to invoke the evaluation. The stability of this string is not 17 | * guaranteed and should not be used in conditional logic. 18 | */ 19 | public final String method; 20 | 21 | /** 22 | * The key of the feature flag being evaluated. 23 | */ 24 | public final String flagKey; 25 | 26 | /** 27 | * The context the evaluation was for. 28 | */ 29 | public final LDContext context; 30 | 31 | /** 32 | * The user-provided default value for the evaluation. 33 | */ 34 | public final LDValue defaultValue; 35 | 36 | /** 37 | * @param method the variation method that was used to invoke the evaluation. 38 | * @param key the key of the feature flag being evaluated. 39 | * @param context the context the evaluation was for. 40 | * @param defaultValue the user-provided default value for the evaluation. 41 | */ 42 | public EvaluationSeriesContext(String method, String key, LDContext context, LDValue defaultValue) { 43 | this.flagKey = key; 44 | this.context = context; 45 | this.defaultValue = defaultValue; 46 | this.method = method; 47 | } 48 | 49 | @Override 50 | public boolean equals(Object obj) { 51 | if (this == obj) return true; 52 | if (obj == null || getClass() != obj.getClass()) return false; 53 | EvaluationSeriesContext other = (EvaluationSeriesContext)obj; 54 | return 55 | Objects.equals(method, other.method) && 56 | Objects.equals(flagKey, other.flagKey) && 57 | Objects.equals(context, other.context) && 58 | Objects.equals(defaultValue, other.defaultValue); 59 | } 60 | 61 | @Override 62 | public int hashCode() { 63 | return Objects.hash(method, flagKey, context, defaultValue); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HookMetadata.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | /** 4 | * Metadata about the {@link Hook} implementation. 5 | */ 6 | public abstract class HookMetadata { 7 | 8 | private final String name; 9 | 10 | public HookMetadata(String name) { 11 | this.name = name; 12 | } 13 | 14 | /** 15 | * @return a friendly name for the {@link Hook} this {@link HookMetadata} belongs to. 16 | */ 17 | public String getName() { 18 | return name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HooksConfigurationBuilder.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.sdk.android.Components; 4 | import com.launchdarkly.sdk.android.subsystems.HookConfiguration; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collections; 8 | import java.util.List; 9 | 10 | /** 11 | * Contains methods for configuring the SDK's 'hooks'. 12 | *

13 | * If you want to add hooks, use {@link Components#hooks()}, configure accordingly, and pass it 14 | * to {@link com.launchdarkly.sdk.android.LDConfig.Builder#hooks(HooksConfigurationBuilder)}. 15 | * 16 | *


17 |  *     List hooks = createSomeHooks();
18 |  *     LDConfig config = new LDConfig.Builder()
19 |  *         .hooks(
20 |  *             Components.hooks()
21 |  *                 .setHooks(hooks)
22 |  *         )
23 |  *         .build();
24 |  * 
25 | *

26 | * Note that this class is abstract; the actual implementation is created by calling {@link Components#hooks()}. 27 | */ 28 | public abstract class HooksConfigurationBuilder { 29 | 30 | /** 31 | * The current set of hooks the builder has. 32 | */ 33 | protected List hooks = Collections.emptyList(); 34 | 35 | /** 36 | * Adds the provided list of hooks to the configuration. Note that the order of hooks is important and controls 37 | * the order in which they will be executed. See {@link Hook} for more details. 38 | * 39 | * @param hooks to be added to the configuration 40 | * @return the builder 41 | */ 42 | public HooksConfigurationBuilder setHooks(List hooks) { 43 | // copy to avoid list manipulations impacting the SDK 44 | this.hooks = Collections.unmodifiableList(new ArrayList<>(hooks)); 45 | return this; 46 | } 47 | 48 | /** 49 | * @return the hooks configuration 50 | */ 51 | abstract public HookConfiguration build(); 52 | } -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/IdentifySeriesContext.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | 5 | import java.util.Objects; 6 | 7 | /** 8 | * Represents parameters associated with calling identify. An instance of this class is provided to some 9 | * stages of series of a {@link Hook} implementation. For example, see {@link Hook#afterTrack(TrackSeriesContext)} 10 | */ 11 | public class IdentifySeriesContext { 12 | /** 13 | * The context associated with the identify operation. 14 | */ 15 | public final LDContext context; 16 | 17 | /** 18 | * The timeout, in seconds, associated with the identify operation. 19 | */ 20 | public final Integer timeout; 21 | 22 | public IdentifySeriesContext(LDContext context, Integer timeout) { 23 | this.context = context; 24 | this.timeout = timeout; 25 | } 26 | 27 | @Override 28 | public boolean equals(Object obj) { 29 | if (this == obj) return true; 30 | if (obj == null || getClass() != obj.getClass()) return false; 31 | IdentifySeriesContext other = (IdentifySeriesContext)obj; 32 | return 33 | Objects.equals(context, other.context) && 34 | Objects.equals(timeout, other.timeout); 35 | } 36 | 37 | @Override 38 | public int hashCode() { 39 | return Objects.hash(context, timeout); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/IdentifySeriesResult.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * The result applies to a single identify operation. An operation may complete 7 | * with an error and then later complete successfully. Only the first completion 8 | * will be executed in the identify series. 9 | *

10 | * For example, a network issue may cause an identify to error since the SDK 11 | * can't refresh its cached data from the cloud at that moment, but then later 12 | * the when the network issue is resolved, the SDK will refresh cached data. 13 | */ 14 | public class IdentifySeriesResult { 15 | /** 16 | * The status an identify operation completed with. 17 | *

18 | * An example in which an error may occur is lack of network connectivity 19 | * preventing the SDK from functioning. 20 | */ 21 | public enum IdentifySeriesStatus { 22 | COMPLETED, 23 | ERROR 24 | } 25 | 26 | public final IdentifySeriesStatus status; 27 | 28 | public IdentifySeriesResult(IdentifySeriesStatus status) { 29 | this.status = status; 30 | } 31 | 32 | @Override 33 | public boolean equals(Object obj) { 34 | if (this == obj) return true; 35 | if (obj == null || getClass() != obj.getClass()) return false; 36 | IdentifySeriesResult other = (IdentifySeriesResult)obj; 37 | return status == other.status; 38 | } 39 | 40 | @Override 41 | public int hashCode() { 42 | return Objects.hash(status); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/TrackSeriesContext.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.sdk.LDContext; 4 | import com.launchdarkly.sdk.LDValue; 5 | 6 | import java.util.Objects; 7 | 8 | /** 9 | * Represents parameters associated with tracking a custom event. An instance of this class is provided to some 10 | * stages of series of a {@link Hook} implementation. For example, see {@link Hook#afterTrack(TrackSeriesContext)} 11 | */ 12 | public class TrackSeriesContext { 13 | /** 14 | * The key for the event being tracked. 15 | */ 16 | public final String key; 17 | 18 | /** 19 | * The context associated with the track operation. 20 | */ 21 | public final LDContext context; 22 | 23 | /** 24 | * The data associated with the track operation. 25 | */ 26 | public final LDValue data; 27 | 28 | /** 29 | * The metric value associated with the track operation. 30 | */ 31 | public final Double metricValue; 32 | 33 | /** 34 | * @param key the key for the event being tracked. 35 | * @param context the context associated with the track operation. 36 | * @param data the data associated with the track operation. 37 | * @param metricValue the metric value associated with the track operation. 38 | */ 39 | public TrackSeriesContext(String key, LDContext context, LDValue data, Double metricValue) { 40 | this.key = key; 41 | this.context = context; 42 | this.data = data; 43 | this.metricValue = metricValue; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object obj) { 48 | if (this == obj) return true; 49 | if (obj == null || getClass() != obj.getClass()) return false; 50 | TrackSeriesContext other = (TrackSeriesContext)obj; 51 | return 52 | Objects.equals(key, other.key) && 53 | Objects.equals(context, other.context) && 54 | Objects.equals(data, other.data) && 55 | Objects.equals(metricValue, other.metricValue); 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return Objects.hash(key, context, data, metricValue); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains integration tools for connecting the SDK to other software components, or 3 | * configuring how it connects to LaunchDarkly. 4 | */ 5 | package com.launchdarkly.sdk.android.integrations; 6 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/interfaces/ServiceEndpoints.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.interfaces; 2 | 3 | import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; 4 | import java.net.URI; 5 | 6 | /** 7 | * Specifies the base service URIs used by SDK components. 8 | *

9 | * See {@link ServiceEndpointsBuilder} for more details on these properties. 10 | * 11 | * @since 4.0.0 12 | */ 13 | public final class ServiceEndpoints { 14 | private final URI streamingBaseUri; 15 | private final URI pollingBaseUri; 16 | private final URI eventsBaseUri; 17 | 18 | /** 19 | * Used internally by the SDK to store service endpoints. 20 | * @param streamingBaseUri the base URI for the streaming service 21 | * @param pollingBaseUri the base URI for the polling service 22 | * @param eventsBaseUri the base URI for the events service 23 | */ 24 | public ServiceEndpoints(URI streamingBaseUri, URI pollingBaseUri, URI eventsBaseUri) { 25 | this.streamingBaseUri = streamingBaseUri; 26 | this.pollingBaseUri = pollingBaseUri; 27 | this.eventsBaseUri = eventsBaseUri; 28 | } 29 | 30 | /** 31 | * The base URI for the streaming service. 32 | * @return the base URI, or null 33 | */ 34 | public URI getStreamingBaseUri() { 35 | return streamingBaseUri; 36 | } 37 | 38 | /** 39 | * The base URI for the polling service. 40 | * @return the base URI, or null 41 | */ 42 | public URI getPollingBaseUri() { 43 | return pollingBaseUri; 44 | } 45 | 46 | /** 47 | * The base URI for the events service. 48 | * @return the base URI, or null 49 | */ 50 | public URI getEventsBaseUri() { 51 | return eventsBaseUri; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/interfaces/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Types that are part of the public API, but are not needed for basic use of the SDK. 3 | *

4 | * Types in this namespace include: 5 | *

    6 | *
  • Interfaces that provide a facade for some part of the SDK API.
  • 7 | *
  • Concrete types that are used as parameters within these interfaces, or as containers for 8 | * pieces of SDK configuration, like {@link com.launchdarkly.sdk.android.interfaces.ServiceEndpoints}.
  • 9 | *
10 | */ 11 | package com.launchdarkly.sdk.android.interfaces; 12 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Main package for the LaunchDarkly Android SDK, containing the client and configuration classes. 3 | *

4 | * You will most often use {@link com.launchdarkly.sdk.android.LDClient} (the SDK client) and 5 | * {@link com.launchdarkly.sdk.android.LDConfig} (configuration options for the client). 6 | *

7 | * Other commonly used types such as {@link com.launchdarkly.sdk.LDContext} are in the {@code com.launchdarkly.sdk} 8 | * package, since those are not Android-specific and are shared with the LaunchDarkly Java server-side SDK. 9 | */ 10 | package com.launchdarkly.sdk.android; 11 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ApplicationInfo.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import androidx.annotation.Nullable; 4 | import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; 5 | 6 | /** 7 | * Encapsulates the SDK's application metadata. 8 | *

9 | * See {@link ApplicationInfoBuilder} for more details on these properties. 10 | * 11 | * @since 4.1.0 12 | */ 13 | public final class ApplicationInfo { 14 | 15 | @Nullable 16 | private final String applicationId; 17 | @Nullable 18 | private final String applicationName; 19 | @Nullable 20 | private final String applicationVersion; 21 | @Nullable 22 | private final String applicationVersionName; 23 | 24 | /** 25 | * Used internally by the SDK to store application metadata. 26 | * 27 | * @param applicationId the application ID 28 | * @param applicationVersion the application version 29 | * @param applicationName friendly name for the application 30 | * @param applicationVersionName friendly name for the version 31 | * @see ApplicationInfoBuilder 32 | */ 33 | public ApplicationInfo(String applicationId, String applicationVersion, 34 | String applicationName, String applicationVersionName) { 35 | this.applicationId = applicationId; 36 | this.applicationVersion = applicationVersion; 37 | this.applicationName = applicationName; 38 | this.applicationVersionName = applicationVersionName; 39 | } 40 | 41 | /** 42 | * A unique identifier representing the application where the LaunchDarkly SDK is running. 43 | * 44 | * @return the application identifier, or null 45 | */ 46 | @Nullable 47 | public String getApplicationId() { 48 | return applicationId; 49 | } 50 | 51 | /** 52 | * A unique identifier representing the version of the application where the 53 | * LaunchDarkly SDK is running. 54 | * 55 | * @return the application version, or null 56 | */ 57 | @Nullable 58 | public String getApplicationVersion() { 59 | return applicationVersion; 60 | } 61 | 62 | /** 63 | * A human friendly name for the application in which the LaunchDarkly SDK is running. 64 | * 65 | * @return the friendly name of the application, or null 66 | */ 67 | @Nullable 68 | public String getApplicationName() { 69 | return applicationName; 70 | } 71 | 72 | /** 73 | * A human friendly name for the version of the application in which the LaunchDarkly SDK is running. 74 | * 75 | * @return the friendly name of the version, or null 76 | */ 77 | @Nullable 78 | public String getApplicationVersionName() { 79 | return applicationVersionName; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/Callback.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | /** 4 | * General-purpose interface for callbacks that can succeed or fail. 5 | * @param the return value type 6 | * @since 4.0.0 7 | */ 8 | public interface Callback { 9 | /** 10 | * This method is called on successful completion. 11 | * @param result the return value 12 | */ 13 | void onSuccess(T result); 14 | 15 | /** 16 | * This method is called on failure. 17 | * @param error the error/exception object 18 | */ 19 | void onError(Throwable error); 20 | } 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ComponentConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | /** 4 | * The common interface for SDK component factories and configuration builders. Applications should not 5 | * need to implement this interface. 6 | * 7 | * @param the type of SDK component or configuration object being constructed 8 | * @since 3.3.0 9 | */ 10 | public interface ComponentConfigurer { 11 | /** 12 | * Called internally by the SDK to create an implementation instance. Applications should not need 13 | * to call this method. 14 | * 15 | * @param clientContext provides configuration properties and other components from the current 16 | * SDK client instance 17 | * @return a instance of the component type 18 | */ 19 | T build(ClientContext clientContext); 20 | } 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSource.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.sdk.LDContext; 6 | import com.launchdarkly.sdk.android.Components; 7 | import com.launchdarkly.sdk.android.LDConfig; 8 | import com.launchdarkly.sdk.android.LDConfig.Builder; 9 | 10 | import java.io.Closeable; 11 | import java.util.concurrent.Callable; 12 | import java.util.concurrent.Future; 13 | 14 | /** 15 | * Interface for an object that receives updates to feature flags from LaunchDarkly. 16 | *

17 | * This component uses a push model. When it is created, the SDK will provide a reference to a 18 | * {@link DataSourceUpdateSink} component (as part of {@link ClientContext}, which is a write-only 19 | * abstraction of the SDK state. The SDK never requests feature flag data from the 20 | * {@link DataSource}-- it only looks at the last known data that was pushed into the state. 21 | *

22 | * Each {@code LDClient} instance maintains exactly one active data source instance. It stops and 23 | * discards the active data source whenever it needs to create a new one due to a significant state 24 | * change, such as if the evaluation context is changed with {@code identify()}, or if the SDK goes 25 | * online after previously being offline, or if the foreground/background state changes. 26 | * 27 | * @since 3.3.0 28 | * @see Components#streamingDataSource() 29 | * @see Components#pollingDataSource() 30 | * @see LDConfig.Builder#dataSource(ComponentConfigurer) 31 | */ 32 | public interface DataSource { 33 | /** 34 | * Initializes the data source. This is called only once per instance. 35 | * @param resultCallback called when the data source has successfully acquired the initial data, 36 | * or if an error has occurred 37 | */ 38 | void start(@NonNull Callback resultCallback); 39 | 40 | /** 41 | * Tells the data source to stop. 42 | * @param completionCallback called once it has completely stopped (this is allowed to be 43 | * asynchronous because it might involve network operations that can't 44 | * be done on the main thread) 45 | */ 46 | void stop(@NonNull Callback completionCallback); 47 | 48 | /** 49 | * The SDK calls this method to determine whether it needs to stop the current data source and 50 | * start a new one after a state transition. 51 | *

52 | * State transitions include going from foreground to background or vice versa, or changing the 53 | * evaluation context. The SDK will not call this method unless at least one of those types of 54 | * transitions has happened. 55 | *

56 | * If this method returns true, the SDK considers the current data source to be no longer valid, 57 | * stops it, and asks the ComponentConfigurer to create a new one. 58 | *

59 | * If this method returns false, the SDK retains the current data source. 60 | * 61 | * @param newInBackground true if the application is now in the background 62 | * @param newEvaluationContext the new evaluation context 63 | * @return true if the data source should be recreated 64 | * @since 4.1.0 65 | */ 66 | default boolean needsRefresh( 67 | boolean newInBackground, 68 | LDContext newEvaluationContext 69 | ) { 70 | return true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSourceUpdateSink.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.annotation.Nullable; 5 | 6 | import com.launchdarkly.sdk.LDContext; 7 | import com.launchdarkly.sdk.android.ConnectionInformation; 8 | import com.launchdarkly.sdk.android.DataModel; 9 | 10 | import java.util.Map; 11 | 12 | /** 13 | * Interface that an implementation of {@link DataSource} will use to push data into the SDK. 14 | * 15 | * @since 4.0.0 16 | */ 17 | public interface DataSourceUpdateSink { 18 | /** 19 | * Completely overwrites the current contents of the data store with a new set of items. 20 | * 21 | * @param items a map of flag keys to flag evaluation results 22 | */ 23 | void init(@NonNull LDContext context, @NonNull Map items); 24 | 25 | /** 26 | * Updates or inserts an item. If an item already exists with the same key, the operation will 27 | * only succeed if the existing version is less than the new version. 28 | *

29 | * If a flag has been deleted, the data source should pass a versioned placeholder created with 30 | * {@link DataModel.Flag#deletedItemPlaceholder(String, int)}. 31 | * 32 | * @param item the new evaluation result data (or a deleted item placeholder) 33 | */ 34 | void upsert(@NonNull LDContext context, @NonNull DataModel.Flag item); 35 | 36 | /** 37 | * Informs the SDK of a change in the data source's status or the connection mode. 38 | * 39 | * @param connectionMode the value that should be reported by 40 | * {@link ConnectionInformation#getConnectionMode()} 41 | * @param failure if non-null, represents an error/exception that caused data source 42 | * initialization to fail 43 | */ 44 | void setStatus(@NonNull ConnectionInformation.ConnectionMode connectionMode, @Nullable Throwable failure); 45 | 46 | /** 47 | * Informs the SDK that the data source is being permanently shut down due to an unrecoverable 48 | * problem reported by LaunchDarkly, such as the mobile key being invalid. 49 | *

50 | * This implies that the SDK should also stop other components that communicate with 51 | * LaunchDarkly, such as the event processor. It also changes the connection mode to 52 | * {@link com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode#SHUTDOWN}. 53 | */ 54 | void shutDown(); 55 | } 56 | 57 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DiagnosticDescription.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import com.launchdarkly.sdk.LDValue; 4 | 5 | /** 6 | * Optional interface for components to describe their own configuration. 7 | *

8 | * The SDK uses a simplified JSON representation of its configuration when recording diagnostics data. 9 | * Any class that implements {@link ComponentConfigurer} may choose to contribute 10 | * values to this representation, although the SDK may or may not use them. For components that do not 11 | * implement this interface, the SDK may instead describe them using {@code getClass().getSimpleName()}. 12 | *

13 | * The {@link #describeConfiguration(ClientContext)} method should return either null or a JSON value. For 14 | * custom components, the value must be a string that describes the basic nature of this component 15 | * implementation (e.g. "Redis"). Built-in LaunchDarkly components may instead return a JSON object 16 | * containing multiple properties specific to the LaunchDarkly diagnostic schema. 17 | * 18 | * @since 3.3.0 19 | */ 20 | public interface DiagnosticDescription { 21 | /** 22 | * Used internally by the SDK to inspect the configuration. 23 | * @param clientContext allows access to the client configuration 24 | * @return an {@link LDValue} or null 25 | */ 26 | LDValue describeConfiguration(ClientContext clientContext); 27 | } 28 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HookConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder; 4 | import com.launchdarkly.sdk.android.integrations.Hook; 5 | 6 | import java.util.Collections; 7 | import java.util.List; 8 | 9 | /** 10 | * Encapsulates the SDK's 'hooks' configuration. 11 | *

12 | * Use {@link HooksConfigurationBuilder} to construct an instance. 13 | */ 14 | public class HookConfiguration { 15 | 16 | private final List hooks; 17 | 18 | /** 19 | * @param hooks the list of {@link Hook} that will be registered. 20 | */ 21 | public HookConfiguration(List hooks) { 22 | this.hooks = Collections.unmodifiableList(hooks); 23 | } 24 | 25 | /** 26 | * @return an immutable list of hooks 27 | */ 28 | public List getHooks() { 29 | return hooks; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HttpConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import com.launchdarkly.sdk.android.LDHeaderUpdater; 4 | import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static java.util.Collections.emptyMap; 10 | 11 | /** 12 | * Encapsulates top-level HTTP configuration that applies to all SDK components. 13 | *

14 | * Use {@link HttpConfigurationBuilder} to construct an instance. 15 | *

16 | * The SDK's built-in components use OkHttp as the HTTP client implementation, but since OkHttp types 17 | * are not surfaced in the public API and custom components might use some other implementation, this 18 | * class only provides the properties that would be used to create an HTTP client; it does not create 19 | * the client itself. SDK implementation code uses its own helper methods to do so. 20 | * 21 | * @since 3.3.0 22 | */ 23 | public final class HttpConfiguration { 24 | private final int connectTimeoutMillis; 25 | private final Map defaultHeaders; 26 | private final LDHeaderUpdater headerTransform; 27 | private final boolean useReport; 28 | 29 | /** 30 | * Creates an instance. 31 | * 32 | * @param connectTimeoutMillis see {@link #getConnectTimeoutMillis()} 33 | * @param defaultHeaders see {@link #getDefaultHeaders()} 34 | * @param headerTransform see {@link #getHeaderTransform()} 35 | * @param useReport see {@link #isUseReport()} 36 | */ 37 | public HttpConfiguration( 38 | int connectTimeoutMillis, 39 | Map defaultHeaders, 40 | LDHeaderUpdater headerTransform, 41 | boolean useReport 42 | ) { 43 | super(); 44 | this.connectTimeoutMillis = connectTimeoutMillis; 45 | this.defaultHeaders = defaultHeaders == null ? emptyMap() : new HashMap<>(defaultHeaders); 46 | this.headerTransform = headerTransform; 47 | this.useReport = useReport; 48 | } 49 | 50 | /** 51 | * The connection timeout. This is the time allowed for the underlying HTTP client to connect 52 | * to the LaunchDarkly server. 53 | * 54 | * @return the connection timeout in milliseconds 55 | */ 56 | public int getConnectTimeoutMillis() { 57 | return connectTimeoutMillis; 58 | } 59 | 60 | /** 61 | * Returns the basic headers that should be added to all HTTP requests from SDK components to 62 | * LaunchDarkly services, based on the current SDK configuration. 63 | * 64 | * @return a list of HTTP header names and values 65 | */ 66 | public Iterable> getDefaultHeaders() { 67 | return defaultHeaders.entrySet(); 68 | } 69 | 70 | /** 71 | * Returns the callback for modifying request headers, if any. 72 | * 73 | * @return the callback for modifying request headers 74 | */ 75 | public LDHeaderUpdater getHeaderTransform() { 76 | return headerTransform; 77 | } 78 | 79 | /** 80 | * The setting for whether to use the HTTP REPORT method. 81 | * 82 | * @return true to use HTTP REPORT 83 | */ 84 | public boolean isUseReport() { 85 | return useReport; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/PersistentDataStore.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.subsystems; 2 | 3 | import java.util.Collection; 4 | import java.util.Map; 5 | 6 | /** 7 | * Interface for a data store that holds feature flag data and other SDK properties in a simple 8 | * string format. 9 | *

10 | * The SDK has a default implementation which uses the Android {@code SharedPreferences} API. A 11 | * custom implementation of this interface could store data somewhere else, or use that API in a 12 | * different way. 13 | *

14 | * Each data item is uniquely identified by the combination of a "namespace" and a "key", and has 15 | * a string value. These are defined as follows: 16 | *

    17 | *
  • Both the namespace and the key are non-empty strings.
  • 18 | *
  • Both the namespace and the key contain only alphanumeric characters, hyphens, and 19 | * underscores.
  • 20 | *
  • The value can be any non-null string, including an empty string.
  • 21 | *
22 | *

23 | * The store implementation does not need to worry about adding a LaunchDarkly-specific prefix to 24 | * namespaces to distinguish them from storage that is used for other purposes; the SDK will take 25 | * care of that at a higher level. PersistentDataStore is just a low-level storage mechanism. 26 | *

27 | * The SDK will also provide its own caching layer on top of the persistent data store; the data 28 | * store implementation should not provide caching, but simply do every query or update that the 29 | * SDK tells it to do. 30 | *

31 | * Error handling is defined as follows: if any data store operation encounters an I/O error, or 32 | * is otherwise unable to complete its task, it should throw an exception to make the SDK aware 33 | * of this. The SDK will decide whether to log the exception. 34 | * 35 | * @since 4.0.0 36 | */ 37 | public interface PersistentDataStore { 38 | /** 39 | * Attempts to retrieve a string value from the store. 40 | * 41 | * @param storeNamespace the namespace identifier 42 | * @param key the unique key within that namespace 43 | * @return the value, or null if not found 44 | */ 45 | String getValue(String storeNamespace, String key); 46 | 47 | /** 48 | * Attempts to update or remove a string value in the store. 49 | * 50 | * @param storeNamespace the namespace identifier 51 | * @param key the unique key within that namespace 52 | * @param value the new value, or null to remove the key 53 | */ 54 | void setValue(String storeNamespace, String key, String value); 55 | 56 | /** 57 | * Attempts to update multiple values atomically. 58 | * 59 | * @param storeNamespace the namespace identifier 60 | * @param keysAndValues the keys and values to update 61 | */ 62 | void setValues(String storeNamespace, Map keysAndValues); 63 | 64 | /** 65 | * Returns all keys that exist in the namespace. 66 | * 67 | * @param storeNamespace the namespace identifier 68 | * @return the keys 69 | */ 70 | Collection getKeys(String storeNamespace); 71 | 72 | /** 73 | * Returns all namespaces that exist in the data store. 74 | *

75 | * This may be an inefficient operation, but the SDK will not call this method on a regular 76 | * basis. It is used only when migrating data from earlier SDK versions. 77 | * 78 | * @return the namespaces 79 | */ 80 | Collection getAllNamespaces(); 81 | 82 | /** 83 | * Removes any values that currently exist in the given namespace. 84 | * 85 | * @param storeNamespace the namespace identifier 86 | * @param fullyDelete true to purge all data structures related to the namespace, false to 87 | * simply leave it empty 88 | */ 89 | void clear(String storeNamespace, boolean fullyDelete); 90 | } 91 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Interfaces for implementation of LaunchDarkly SDK components. 3 | *

4 | * Most applications will not need to refer to these types. You will use them if you are creating a 5 | * plugin component, such as a database integration. They are also used as interfaces for the built-in 6 | * SDK components, so that plugin components can be used interchangeably with those. 7 | *

8 | * The package also includes concrete types that are used as parameters within these interfaces. 9 | */ 10 | package com.launchdarkly.sdk.android.subsystems; 11 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/main/resources/android-logger.properties: -------------------------------------------------------------------------------- 1 | # Android Logger configuration example 2 | 3 | # By default logger will print only ERROR (and higher) messages 4 | # with "MyApplication" tag 5 | #root=ERROR:MyApplication 6 | 7 | # DEBUG (and higher) messages from classes of com.example.database 8 | # will be logged with "MyApplication-Database" tag 9 | logger.com.launchdarkly.eventsource=INFO:EventSource 10 | 11 | # All messages from classes of com.example.ui will be logged with 12 | # "MyApplication-UI" tag 13 | #logger.com.example.ui=MyApplication-UI -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/android/util/Base64.java: -------------------------------------------------------------------------------- 1 | package android.util; 2 | 3 | // This file exists only to support the unit tests in src/test/java. The issue is that the SDK 4 | // code uses android.util.Base64, which only exists in the Android runtime library; but the unit 5 | // tests (as opposed to the instrumented tests in src/androidTest/java) run against the regular 6 | // Java runtime library. The solution is to put an android.util.Base64 class in the classpath that 7 | // simply delegates to java.util.Base64. 8 | // 9 | // We can't simply change the SDK code to use java.util.Base64 because that is only available in 10 | // Android API 26 and above. 11 | 12 | public class Base64 { 13 | public static String encodeToString(byte[] input, int flags) { 14 | return java.util.Base64.getEncoder().encodeToString(input); 15 | } 16 | 17 | public static byte[] decode(String str, int flags) { 18 | return java.util.Base64.getDecoder().decode(str); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/android/util/Pair.java: -------------------------------------------------------------------------------- 1 | package android.util; 2 | 3 | // This file exists only to support the unit tests in src/test/java. The issue is that the SDK 4 | // code uses android.util.Pair, which only exists in the Android runtime library; but the unit 5 | // tests (as opposed to the instrumented tests in src/androidTest/java) run against the regular 6 | // Java runtime library. The solution is to put an android.util.Pair class in the classpath that 7 | // is an exact copy of the real android.util.Pair. 8 | 9 | import androidx.annotation.Nullable; 10 | 11 | import java.util.Objects; 12 | 13 | public class Pair { 14 | public final F first; 15 | public final S second; 16 | /** 17 | * Constructor for a Pair. 18 | * 19 | * @param first the first object in the Pair 20 | * @param second the second object in the pair 21 | */ 22 | public Pair(F first, S second) { 23 | this.first = first; 24 | this.second = second; 25 | } 26 | /** 27 | * Checks the two objects for equality by delegating to their respective 28 | * {@link Object#equals(Object)} methods. 29 | * 30 | * @param o the {@link Pair} to which this one is to be checked for equality 31 | * @return true if the underlying objects of the Pair are both considered 32 | * equal 33 | */ 34 | @Override 35 | public boolean equals(@Nullable Object o) { 36 | if (!(o instanceof Pair)) { 37 | return false; 38 | } 39 | Pair p = (Pair) o; 40 | return Objects.equals(p.first, first) && Objects.equals(p.second, second); 41 | } 42 | /** 43 | * Compute a hash code using the hash codes of the underlying objects 44 | * 45 | * @return a hashcode of the Pair 46 | */ 47 | @Override 48 | public int hashCode() { 49 | return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); 50 | } 51 | @Override 52 | public String toString() { 53 | return "Pair{" + String.valueOf(first) + " " + String.valueOf(second) + "}"; 54 | } 55 | /** 56 | * Convenience method for creating an appropriately typed pair. 57 | * @param a the first object in the Pair 58 | * @param b the second object in the pair 59 | * @return a Pair that is templatized with the types of a and b 60 | */ 61 | public static Pair create(A a, B b) { 62 | return new Pair(a, b); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/ContextDataManagerContextCachingTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static com.launchdarkly.sdk.android.AssertHelpers.assertDataSetsEqual; 4 | import static com.launchdarkly.sdk.android.AssertHelpers.assertFlagsEqual; 5 | import static org.junit.Assert.assertEquals; 6 | import static org.junit.Assert.assertNotEquals; 7 | import static org.junit.Assert.assertNotNull; 8 | import static org.junit.Assert.assertNull; 9 | import static org.junit.Assert.assertSame; 10 | import static org.junit.Assert.fail; 11 | 12 | import com.launchdarkly.sdk.LDContext; 13 | import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; 14 | 15 | import org.junit.Rule; 16 | import org.junit.Test; 17 | 18 | public class ContextDataManagerContextCachingTest extends ContextDataManagerTestBase { 19 | @Test 20 | public void deletePreviousDataAfterSwitchForZeroCached() { 21 | ContextDataManager manager = createDataManager(0); 22 | 23 | for (int i = 1; i <= 2; i++) { 24 | manager.initData(makeContext(i), makeFlagData(i)); 25 | } 26 | 27 | assertContextIsNotCached(makeContext(1)); 28 | } 29 | 30 | @Test 31 | public void canCacheManyContextsWithNegativeMaxCachedContexts() { 32 | ContextDataManager manager = createDataManager(-1); 33 | 34 | int numContexts = 20; 35 | for (int i = 1; i <= numContexts; i++) { 36 | manager.switchToContext(makeContext(i)); 37 | manager.initData(makeContext(i), makeFlagData(i)); 38 | } 39 | 40 | for (int i = 1; i <= numContexts; i++) { 41 | assertContextIsCached(makeContext(i), makeFlagData(i)); 42 | } 43 | assertEquals(numContexts, environmentStore.getIndex().data.size()); 44 | } 45 | 46 | @Test 47 | public void deletesExcessContexts() { 48 | int maxCachedContexts = 10, excess = 2; 49 | ContextDataManager manager = createDataManager(maxCachedContexts); 50 | 51 | for (int i = 1; i <= maxCachedContexts + excess; i++) { 52 | manager.switchToContext(makeContext(i)); 53 | manager.initData(makeContext(i), makeFlagData(i)); 54 | } 55 | 56 | for (int i = 1; i <= excess; i++) { 57 | assertContextIsNotCached(makeContext(i)); 58 | } 59 | for (int i = excess + 1; i <= maxCachedContexts + excess; i++) { 60 | assertContextIsCached(makeContext(i), makeFlagData(i)); 61 | } 62 | } 63 | 64 | @Test 65 | public void deletesExcessContextsFromPreviousManagerInstance() { 66 | ContextDataManager manager = createDataManager(1); 67 | 68 | for (int i = 1; i <= 2; i++) { 69 | manager.switchToContext(makeContext(i)); 70 | manager.initData(makeContext(i), makeFlagData(i)); 71 | assertContextIsCached(makeContext(i), makeFlagData(i)); 72 | } 73 | 74 | ContextDataManager newManagerInstance = createDataManager(1); 75 | newManagerInstance.switchToContext(makeContext(3)); 76 | newManagerInstance.initData(makeContext(3), makeFlagData(3)); 77 | 78 | assertContextIsNotCached(makeContext(1)); 79 | assertContextIsNotCached(makeContext(2)); 80 | assertContextIsCached(makeContext(3), makeFlagData(3)); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DebounceTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.Debounce; 4 | 5 | import org.junit.Test; 6 | 7 | import static org.junit.Assert.assertEquals; 8 | 9 | public class DebounceTest { 10 | 11 | @Test 12 | public void callPendingNullReturnNoAction() { 13 | Debounce test = new Debounce(); 14 | int expected = 0; 15 | int actual = 0; 16 | 17 | test.call(null); 18 | 19 | assertEquals(expected, actual); 20 | } 21 | 22 | @Test 23 | public void callPendingSetReturnOne() throws InterruptedException { 24 | Debounce test = new Debounce(); 25 | Integer expected = 1; 26 | final Integer[] actual = {0}; 27 | 28 | test.call(() -> { 29 | actual[0] = 1; 30 | return null; 31 | }); 32 | 33 | Thread.sleep(1000); 34 | 35 | assertEquals(expected, actual[0]); 36 | } 37 | 38 | @Test 39 | public void callPendingSetReturnTwo() throws InterruptedException { 40 | Debounce test = new Debounce(); 41 | Integer expected = 2; 42 | final Integer[] actual = {0}; 43 | 44 | test.call(() -> { 45 | actual[0] = 1; 46 | return null; 47 | }); 48 | Thread.sleep(1000); 49 | test.call(() -> { 50 | actual[0] = 2; 51 | return null; 52 | }); 53 | Thread.sleep(1000); 54 | 55 | assertEquals(expected, actual[0]); 56 | } 57 | 58 | @Test 59 | public void callPendingSetReturnThreeBounce() throws InterruptedException { 60 | Debounce test = new Debounce(); 61 | Integer expected = 3; 62 | final Integer[] actual = {0}; 63 | 64 | test.call(() -> { 65 | actual[0] = 1; 66 | Thread.sleep(100); 67 | return null; 68 | }); 69 | test.call(() -> { 70 | actual[0] = 2; 71 | return null; 72 | }); 73 | test.call(() -> { 74 | if (actual[0] == 1) 75 | actual[0] = 3; 76 | return null; 77 | }); 78 | Thread.sleep(500); 79 | 80 | assertEquals(expected, actual[0]); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/HttpConfigurationBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.env.EnvironmentReporterBuilder; 4 | import com.launchdarkly.sdk.android.subsystems.ClientContext; 5 | import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; 6 | 7 | import org.junit.Test; 8 | 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | 12 | import static com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS; 13 | import static org.junit.Assert.assertEquals; 14 | 15 | public class HttpConfigurationBuilderTest { 16 | private static final String MOBILE_KEY = "mobile-key"; 17 | private static final ClientContext BASIC_CONTEXT = new ClientContext(MOBILE_KEY, new EnvironmentReporterBuilder().build(), 18 | null, null, null, "", false, null, null, false, null, null, false); 19 | 20 | private static Map buildBasicHeaders() { 21 | Map ret = new HashMap<>(); 22 | ret.put("Authorization", LDUtil.AUTH_SCHEME + MOBILE_KEY); 23 | ret.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); 24 | ret.put("X-LaunchDarkly-Tags", "application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME 25 | + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME); 26 | return ret; 27 | } 28 | 29 | private static Map toMap(Iterable> entries) { 30 | Map ret = new HashMap<>(); 31 | for (Map.Entry e: entries) { 32 | ret.put(e.getKey(), e.getValue()); 33 | } 34 | return ret; 35 | } 36 | 37 | @Test 38 | public void testDefaults() { 39 | HttpConfiguration hc = Components.httpConfiguration().build(BASIC_CONTEXT); 40 | assertEquals(DEFAULT_CONNECT_TIMEOUT_MILLIS, hc.getConnectTimeoutMillis()); 41 | assertEquals(buildBasicHeaders(), toMap(hc.getDefaultHeaders())); 42 | } 43 | 44 | @Test 45 | public void testConnectTimeout() { 46 | HttpConfiguration hc = Components.httpConfiguration() 47 | .connectTimeoutMillis(999) 48 | .build(BASIC_CONTEXT); 49 | assertEquals(999, hc.getConnectTimeoutMillis()); 50 | } 51 | 52 | @Test 53 | public void testWrapperNameOnly() { 54 | HttpConfiguration hc = Components.httpConfiguration() 55 | .wrapper("Scala", null) 56 | .build(BASIC_CONTEXT); 57 | assertEquals("Scala", toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Wrapper")); 58 | } 59 | 60 | @Test 61 | public void testWrapperWithVersion() { 62 | HttpConfiguration hc = Components.httpConfiguration() 63 | .wrapper("Scala", "0.1.0") 64 | .build(BASIC_CONTEXT); 65 | assertEquals("Scala/0.1.0", toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Wrapper")); 66 | } 67 | 68 | @Test 69 | public void testApplicationTags() { 70 | ClientContext contextWithTags = new ClientContext(MOBILE_KEY, new EnvironmentReporterBuilder().build(), 71 | null, null, null, "", false, null, null, false, null, null, false); 72 | HttpConfiguration hc = Components.httpConfiguration() 73 | .build(contextWithTags); 74 | assertEquals("application-id/" + LDPackageConsts.SDK_NAME + " application-name/" + LDPackageConsts.SDK_NAME 75 | + " application-version/" + BuildConfig.VERSION_NAME + " application-version-name/" + BuildConfig.VERSION_NAME , 76 | toMap(hc.getDefaultHeaders()).get("X-LaunchDarkly-Tags")); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDCompletedFutureTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.LDFailedFuture; 4 | import com.launchdarkly.sdk.android.LDSuccessFuture; 5 | 6 | import org.junit.Test; 7 | 8 | import java.util.concurrent.ExecutionException; 9 | import java.util.concurrent.TimeUnit; 10 | 11 | import static org.junit.Assert.assertFalse; 12 | import static org.junit.Assert.assertSame; 13 | import static org.junit.Assert.assertTrue; 14 | import static org.junit.Assert.fail; 15 | 16 | public class LDCompletedFutureTest { 17 | @Test 18 | public void ldSuccessFuture() { 19 | Object contained = new Object(); 20 | LDSuccessFuture future = new LDSuccessFuture<>(contained); 21 | assertFalse(future.isCancelled()); 22 | assertTrue(future.isDone()); 23 | assertSame(contained, future.get()); 24 | assertSame(contained, future.get(0, TimeUnit.MILLISECONDS)); 25 | assertFalse(future.cancel(false)); 26 | assertFalse(future.cancel(true)); 27 | // Still should be completed, not cancelled, and return the same value 28 | assertFalse(future.isCancelled()); 29 | assertTrue(future.isDone()); 30 | assertSame(contained, future.get()); 31 | assertSame(contained, future.get(0, TimeUnit.MILLISECONDS)); 32 | } 33 | 34 | @Test 35 | public void ldFailedFuture() { 36 | Throwable failure = new Throwable("test"); 37 | LDFailedFuture future = new LDFailedFuture<>(failure); 38 | assertFalse(future.isCancelled()); 39 | assertTrue(future.isDone()); 40 | try { 41 | future.get(); 42 | fail("Expected ExecutionException"); 43 | } catch (ExecutionException ex) { 44 | assertSame(failure, ex.getCause()); 45 | } 46 | try { 47 | future.get(0, TimeUnit.MILLISECONDS); 48 | fail("Expected ExecutionException"); 49 | } catch (ExecutionException ex) { 50 | assertSame(failure, ex.getCause()); 51 | } 52 | assertFalse(future.cancel(false)); 53 | assertFalse(future.cancel(true)); 54 | // Still should be completed, not cancelled, and return the same exception 55 | assertFalse(future.isCancelled()); 56 | assertTrue(future.isDone()); 57 | try { 58 | future.get(); 59 | fail("Expected ExecutionException"); 60 | } catch (ExecutionException ex) { 61 | assertSame(failure, ex.getCause()); 62 | } 63 | try { 64 | future.get(0, TimeUnit.MILLISECONDS); 65 | fail("Expected ExecutionException"); 66 | } catch (ExecutionException ex) { 67 | assertSame(failure, ex.getCause()); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/LDUtilTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Test; 5 | 6 | public class LDUtilTest { 7 | 8 | @Test 9 | public void testUrlSafeBase64Hash() { 10 | String input = "hashThis!"; 11 | String expectedOutput = "sfXg3HewbCAVNQLJzPZhnFKntWYvN0nAYyUWFGy24dQ="; 12 | String output = LDUtil.urlSafeBase64Hash(input); 13 | Assert.assertEquals(expectedOutput, output); 14 | } 15 | 16 | @Test 17 | public void testValidateStringValue() { 18 | Assert.assertNotNull(LDUtil.validateStringValue("")); 19 | Assert.assertNotNull(LDUtil.validateStringValue("0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEFwhoops")); 20 | Assert.assertNotNull(LDUtil.validateStringValue("#@$%^&")); 21 | Assert.assertNull(LDUtil.validateStringValue("a-Az-Z0-9._-")); 22 | } 23 | 24 | @Test 25 | public void testSanitizeSpaces() { 26 | Assert.assertEquals("", LDUtil.sanitizeSpaces("")); 27 | Assert.assertEquals("--hello--", LDUtil.sanitizeSpaces(" hello ")); 28 | Assert.assertEquals("world", LDUtil.sanitizeSpaces("world")); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/MigrationTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static org.easymock.EasyMock.expect; 4 | import static org.hamcrest.CoreMatchers.equalTo; 5 | import static org.hamcrest.CoreMatchers.hasItems; 6 | import static org.hamcrest.MatcherAssert.assertThat; 7 | 8 | import com.launchdarkly.sdk.ContextKind; 9 | import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; 10 | 11 | import org.easymock.EasyMockSupport; 12 | import org.junit.Rule; 13 | import org.junit.Test; 14 | 15 | public class MigrationTest extends EasyMockSupport { 16 | private final PersistentDataStore store = new InMemoryPersistentDataStore(); 17 | 18 | @Rule public LogCaptureRule logging = new LogCaptureRule(); 19 | 20 | @Test 21 | public void doesNothingIfCurrentSchemaIsAlreadyPresent() { 22 | PersistentDataStore mockStore = strictMock(PersistentDataStore.class); 23 | expect(mockStore.getValue(Migration.MIGRATIONS_NAMESPACE, Migration.CURRENT_SCHEMA_ID)) 24 | .andReturn(Migration.CURRENT_SCHEMA_ID); 25 | // will throw an exception if any other methods are called on the store 26 | replayAll(); 27 | 28 | Migration.migrateWhenNeeded(mockStore, logging.logger); 29 | verifyAll(); 30 | } 31 | 32 | @Test 33 | public void deletesAllSdkNamespacesFromOldSchemaAndSetsCurrentSchema() { 34 | String unrelatedNamespace = "not-from-LaunchDarkly", unrelatedKey = "a", unrelatedValue = "b"; 35 | store.setValue(unrelatedNamespace, unrelatedKey, unrelatedValue); 36 | 37 | for (int i = 0; i < 10; i++) { 38 | store.setValue(Migration.SHARED_PREFS_BASE_KEY + "data" + i, "a", "b"); 39 | // all of these should be deleted by migration, which by default will discard anything 40 | // that starts with SHARED_PREFS_BASE_KEY 41 | } 42 | 43 | Migration.migrateWhenNeeded(store, logging.logger); 44 | 45 | assertThat(store.getAllNamespaces().size(), equalTo(2)); 46 | assertCurrentSchemaIdIsPresent(); 47 | 48 | // unrelated namespace shouldn't have been modified 49 | assertThat(store.getKeys(unrelatedNamespace).size(), equalTo(1)); 50 | assertThat(store.getValue(unrelatedNamespace, unrelatedKey), equalTo(unrelatedValue)); 51 | } 52 | 53 | @Test 54 | public void migratesGeneratedAnonUserKey() { 55 | String generatedKey = "key12345"; 56 | store.setValue(Migration.SHARED_PREFS_BASE_KEY + "id", "instanceId", generatedKey); 57 | 58 | Migration.migrateWhenNeeded(store, logging.logger); 59 | 60 | assertThat(store.getAllNamespaces().size(), equalTo(2)); 61 | assertCurrentSchemaIdIsPresent(); 62 | 63 | PersistentDataStoreWrapper w = new PersistentDataStoreWrapper(store, logging.logger); 64 | assertThat(w.getOrGenerateContextKey(ContextKind.DEFAULT), equalTo(generatedKey)); 65 | } 66 | 67 | private void assertCurrentSchemaIdIsPresent() { 68 | assertThat(store.getAllNamespaces(), hasItems(Migration.MIGRATIONS_NAMESPACE)); 69 | assertThat(store.getValue(Migration.MIGRATIONS_NAMESPACE, Migration.CURRENT_SCHEMA_ID), 70 | equalTo(Migration.CURRENT_SCHEMA_ID)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/ApplicationInfoBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.logging.LDLogger; 4 | import com.launchdarkly.sdk.android.subsystems.ApplicationInfo; 5 | 6 | import org.junit.Assert; 7 | import org.junit.Test; 8 | 9 | public class ApplicationInfoBuilderTest { 10 | 11 | @Test 12 | public void ignoresInvalidValues() { 13 | ApplicationInfoBuilder b = new ApplicationInfoBuilder(); 14 | b.logger = LDLogger.none(); 15 | b.applicationId("im#invalid"); 16 | b.applicationName("im#invalid"); 17 | b.applicationVersion("im#invalid"); 18 | b.applicationVersionName("im#invalid"); 19 | ApplicationInfo info = b.createApplicationInfo(); 20 | Assert.assertNull(info.getApplicationId()); 21 | Assert.assertNull(info.getApplicationName()); 22 | Assert.assertNull(info.getApplicationVersion()); 23 | Assert.assertNull(info.getApplicationVersionName()); 24 | } 25 | 26 | @Test 27 | public void sanitizesValues() { 28 | ApplicationInfoBuilder b = new ApplicationInfoBuilder(); 29 | b.logger = LDLogger.none(); 30 | b.applicationId("id has spaces"); 31 | b.applicationName("name has spaces"); 32 | b.applicationVersion("version has spaces"); 33 | b.applicationVersionName("version name has spaces"); 34 | ApplicationInfo info = b.createApplicationInfo(); 35 | Assert.assertEquals("id-has-spaces", info.getApplicationId()); 36 | Assert.assertEquals("name-has-spaces", info.getApplicationName()); 37 | Assert.assertEquals("version-has-spaces", info.getApplicationVersion()); 38 | Assert.assertEquals("version-name-has-spaces", info.getApplicationVersionName()); 39 | } 40 | 41 | @Test 42 | public void nullValueIsValid() { 43 | ApplicationInfoBuilder b = new ApplicationInfoBuilder(); 44 | b.logger = LDLogger.none(); 45 | b.applicationId("myID"); // first non-null 46 | ApplicationInfo info = b.createApplicationInfo(); 47 | Assert.assertEquals("myID", info.getApplicationId()); 48 | 49 | b.applicationId(null); // now back to null 50 | ApplicationInfo info2 = b.createApplicationInfo(); 51 | Assert.assertNull(info2.getApplicationId()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/integrations/HookConfigurationBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android.integrations; 2 | 3 | import com.launchdarkly.sdk.android.Components; 4 | import com.launchdarkly.sdk.android.subsystems.HookConfiguration; 5 | 6 | import org.junit.Test; 7 | 8 | import static org.junit.Assert.assertEquals; 9 | import static org.junit.Assert.assertSame; 10 | import static org.easymock.EasyMock.createMock; 11 | 12 | import java.util.List; 13 | 14 | public class HookConfigurationBuilderTest { 15 | @Test 16 | public void emptyHooksAsDefault() { 17 | HookConfiguration configuration = Components.hooks().build(); 18 | assertEquals(0, configuration.getHooks().size()); 19 | } 20 | 21 | @Test 22 | public void canSetHooks() { 23 | Hook hookA = createMock(Hook.class); 24 | Hook hookB = createMock(Hook.class); 25 | HookConfiguration configuration = Components.hooks().setHooks(List.of(hookA, hookB)).build(); 26 | assertEquals(2, configuration.getHooks().size()); 27 | assertSame(hookA, configuration.getHooks().get(0)); 28 | assertSame(hookB, configuration.getHooks().get(1)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bootstrap-sha": "2b41c9120f53233bada7d1f1e858d5e42acd573a", 3 | "packages": { 4 | ".": { 5 | "release-type": "simple", 6 | "bump-minor-pre-major": true, 7 | "versioning": "default", 8 | "include-v-in-tag": false, 9 | "include-component-in-tag": false, 10 | "extra-files": [ 11 | "gradle.properties" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /scripts/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # This script updates the version for the android-client-sdk library and releases the artifact + javadoc 3 | # It will only work if you have the proper credentials set up in ~/.gradle/gradle.properties 4 | 5 | # It takes exactly one argument: the new version. 6 | # It should be run from the root of this git repo like this: 7 | # ./scripts/release.sh 4.0.9 8 | 9 | # When done you should commit and push the changes made. 10 | 11 | set -uxe 12 | echo "Starting android-client-sdk release." 13 | 14 | VERSION=$1 15 | 16 | # Update version in gradle.properties file: 17 | sed -i.bak "s/version[ ]*=.*$/version = '${VERSION}'/" launchdarkly-android-client-sdk/build.gradle 18 | rm -f launchdarkly-android-client-sdk/build.gradle.bak 19 | 20 | ./gradlew test sourcesJar javadocJar packageRelease 21 | ./gradlew uploadArchives closeAndReleaseRepository 22 | ./gradlew publishGhPages 23 | echo "Finished android-client-sdk release." 24 | -------------------------------------------------------------------------------- /scripts/start-emulator.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Starts an Android emulator to run the contract tests in. 4 | # 5 | # This script assumes that adb, avdmanager, and emulator are on the path. It 6 | # will remember the PID of the last emulator created by this script, and try to kill 7 | # it automatically before starting another one. 8 | # 9 | # This script assumes that you have at least one Android package installed (probably from 10 | # Android Studio), and will automatically pick the latest one to run as an emulator. 11 | # 12 | # Optional params: 13 | # ANDROID_PORT: the Android port, which uniquely identifies a running virtual device. 14 | # If you have another virtual device running (e.g. via Android Studio), 15 | # you may have to override this port number to something else. 16 | # AVD_NAME: the name of the AVD to create and run. 17 | 18 | set -eo pipefail 19 | 20 | ANDROID_PORT=${ANDROID_PORT:-5554} 21 | SERIAL_NUMBER=emulator-${ANDROID_PORT} 22 | AVD_NAME=${AVD_NAME:-launchdarkly-contract-test-emulator} 23 | EMULATOR_PID=.emulator-pid 24 | if [ -z "${AVD_IMAGE}" ]; then 25 | echo "Using the latest installed Android image" 26 | AVD_IMAGE=$(sdkmanager --list_installed | awk '{ print $1 }' | grep system-images | sort -r -k 2 -t ';' | head -1) 27 | if [ -z "${AVD_IMAGE}" ]; then 28 | echo "No emulator images installed locally that meet criteria; try overriding AVD_IMAGE variable" 29 | exit 1 30 | fi 31 | echo "Picked ${AVD_IMAGE}" 32 | fi 33 | 34 | if [ -f ${EMULATOR_PID} ]; then 35 | if ps $(cat ${EMULATOR_PID}); then 36 | echo "Killing previous emulator" 37 | kill -9 $(cat ${EMULATOR_PID}) 38 | fi 39 | rm ${EMULATOR_PID} 40 | fi 41 | 42 | # Create or recreate the AVD. 43 | echo no | avdmanager create avd -n ${AVD_NAME} -f -k "${AVD_IMAGE}" 44 | 45 | # According to https://stackoverflow.com/questions/37063267/high-cpu-usage-with-android-emulator-qemu-system-i386-exe 46 | # the emulator's CPU usage can be greatly reduced by modifying the following properties of the AVD. Not sure if 47 | # that's entirely true - CPU usage seems to go up and down a lot with no apparent cause - but it's worth a try. 48 | AVD_CONFIG=~/.android/avd/${AVD_NAME}.avd/config.ini 49 | echo "hw.audioInput=no" >>${AVD_CONFIG} 50 | echo "hw.audioOutput=no" >>${AVD_CONFIG} 51 | 52 | # Start emulator in background 53 | # Note that for some reason things do not work properly in Ubuntu unless we cd to the directory where the emulator is 54 | EMULATOR_PARAMS="-avd ${AVD_NAME} -port ${ANDROID_PORT} -no-audio -no-snapshot" 55 | emulator $EMULATOR_PARAMS & 56 | EMULATOR_PID=$! 57 | 58 | echo $EMULATOR_PID > .emulator-pid 59 | 60 | # If something goes wrong for the remainder of this script, tear down the emulator automatically 61 | trap "kill -9 $EMULATOR_PID" SIGINT SIGTERM ERR 62 | 63 | # Wait for emulator 64 | 65 | TIMEFORMAT='Emulator started in %R seconds' 66 | 67 | bootanim="" 68 | failcounter=0 69 | timeout_in_sec=360 70 | 71 | echo -n "Waiting for emulator to start" 72 | 73 | time { 74 | until [[ "$bootanim" =~ "stopped" ]]; do 75 | bootanim=`adb -s ${SERIAL_NUMBER} shell getprop init.svc.bootanim 2>&1 &` 76 | if [[ "$bootanim" =~ "device not found" || "$bootanim" =~ "device offline" 77 | || "$bootanim" =~ "running" ]]; then 78 | let "failcounter += 1" 79 | echo -n "." 80 | if [[ $failcounter -gt $timeout_in_sec ]]; then 81 | echo "Timeout ($timeout_in_sec seconds) reached; failed to start emulator" 82 | TIMEFORMAT= 83 | exit 1 84 | fi 85 | fi 86 | sleep 2 87 | done 88 | } 89 | 90 | # Remove lock screen 91 | adb -s ${SERIAL_NUMBER} shell input keyevent 82 92 | -------------------------------------------------------------------------------- /scripts/start-test-service.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Starts the contract test service in the specified Android emulator. 4 | # 5 | # This script assumes that adb is on the path. After it starts the test app on the specified 6 | # emulator, it will wait to confirm that the process appears to be running on the device, and then 7 | # forward the port to localhost, before exiting. 8 | # 9 | # Optional params: 10 | # LOCAL_PORT: the test service will bind to localhost on this port 11 | # ANDROID_PORT: the Android port, which uniquely identifies a running virtual device. 12 | # If you have multiple virtual devices running (e.g. via Android Studio), 13 | # you may have to override this port number to something else. Must be the same 14 | # value as the ANDROID_PORT from start-emulator.sh. 15 | # CONTRACT_TESTS_APK: the contract-tests APK to install and run on the device. 16 | 17 | set -eo pipefail 18 | 19 | LOCAL_PORT=${LOCAL_PORT:-8001} 20 | ANDROID_PORT=${ANDROID_PORT:-5554} 21 | SERIAL_NUMBER=emulator-${ANDROID_PORT} 22 | CONTRACT_TESTS_APK=${CONTRACT_TESTS_APK:-contract-tests/build/outputs/apk/debug/contract-tests-debug.apk} 23 | 24 | # Install APK to emulator 25 | adb -s ${SERIAL_NUMBER} install -t -r -d ${CONTRACT_TESTS_APK} 26 | 27 | # Run APK on emulator 28 | adb -s ${SERIAL_NUMBER} shell am start -n com.launchdarkly.sdktest/.MainActivity -e PORT $LOCAL_PORT 29 | 30 | TIMEFORMAT='App started in %R seconds' 31 | APP_PID="" 32 | time { 33 | while [[ "$APP_PID" == "" ]]; do 34 | APP_PID=`adb -s ${SERIAL_NUMBER} shell pidof -s com.launchdarkly.sdktest` || APP_PID="" 35 | if [[ "$APP_PID" == "" ]]; then sleep 1; fi 36 | done 37 | } 38 | 39 | # Forward connections to emulator 40 | adb -s ${SERIAL_NUMBER} forward tcp:$LOCAL_PORT tcp:$LOCAL_PORT 41 | 42 | echo "Test service started. Run 'adb logcat' to see live log output" 43 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include(":launchdarkly-android-client-sdk") 2 | include(":shared-test-code") 3 | include(":example") 4 | include(":contract-tests") 5 | -------------------------------------------------------------------------------- /shared-test-code/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | id("com.android.library") 4 | id("com.getkeepsafe.dexcount") 5 | } 6 | 7 | group = "com.launchdarkly" 8 | // Specified in gradle.properties 9 | version = version 10 | 11 | ext {} 12 | ext.versions = [ 13 | "androidAnnotation": "1.2.0", 14 | "gson": "2.8.9", 15 | "junit": "4.13", 16 | "launchdarklyLogging": "1.1.1", 17 | ] 18 | 19 | android { 20 | compileSdkVersion(30) 21 | buildToolsVersion = "30.0.3" 22 | 23 | defaultConfig { 24 | minSdkVersion(21) 25 | targetSdkVersion(30) 26 | } 27 | } 28 | 29 | dependencies { 30 | implementation(project(":launchdarkly-android-client-sdk")) 31 | 32 | implementation("androidx.annotation:annotation:${versions.androidAnnotation}") 33 | implementation("com.google.code.gson:gson:${versions.gson}") 34 | implementation("junit:junit:${versions.junit}") 35 | implementation("org.easymock:easymock:4.3") 36 | } 37 | -------------------------------------------------------------------------------- /shared-test-code/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/shared-test-code/consumer-rules.pro -------------------------------------------------------------------------------- /shared-test-code/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /shared-test-code/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/AwaitableCallback.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.subsystems.Callback; 4 | 5 | import java.util.concurrent.CountDownLatch; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.TimeUnit; 8 | import java.util.concurrent.TimeoutException; 9 | 10 | public class AwaitableCallback implements Callback { 11 | private volatile Throwable errResult = null; 12 | private volatile T result = null; 13 | private volatile CountDownLatch signal = new CountDownLatch(1); 14 | 15 | @Override 16 | public void onSuccess(T result) { 17 | this.result = result; 18 | signal.countDown(); 19 | } 20 | 21 | @Override 22 | public void onError(Throwable e) { 23 | errResult = e; 24 | signal.countDown(); 25 | } 26 | 27 | synchronized T await() throws ExecutionException { 28 | try { 29 | signal.await(); 30 | } catch (InterruptedException e) { 31 | throw new ExecutionException(e); 32 | } 33 | if (errResult != null) { 34 | throw new ExecutionException(errResult); 35 | } 36 | return result; 37 | } 38 | 39 | synchronized T await(long timeoutMillis) throws ExecutionException, TimeoutException { 40 | try { 41 | boolean completed = signal.await(timeoutMillis, TimeUnit.MILLISECONDS); 42 | if (!completed) { 43 | throw new TimeoutException(); 44 | } 45 | } catch (InterruptedException e) { 46 | throw new ExecutionException(e); 47 | } 48 | if (errResult != null) { 49 | throw new ExecutionException(errResult); 50 | } 51 | return result; 52 | } 53 | 54 | synchronized void reset() { 55 | signal = new CountDownLatch(1); 56 | errResult = null; 57 | result = null; 58 | } 59 | } -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/DataSetBuilder.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.LDValue; 4 | import com.launchdarkly.sdk.android.DataModel.Flag; 5 | 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | public final class DataSetBuilder { 10 | private final Map flags = new HashMap<>(); 11 | 12 | public static EnvironmentData emptyData() { 13 | return new DataSetBuilder().build(); 14 | } 15 | 16 | public EnvironmentData build() { 17 | return EnvironmentData.copyingFlagsMap(flags); 18 | } 19 | 20 | public DataSetBuilder add(Flag flag) { 21 | flags.put(flag.getKey(), flag); 22 | return this; 23 | } 24 | 25 | public DataSetBuilder add(String flagKey, int version, LDValue value, int variation) { 26 | return add(new Flag(flagKey, value, version, null, variation, 27 | false, false, null, null, null)); 28 | } 29 | 30 | public DataSetBuilder add(String flagKey, LDValue value, int variation) { 31 | return add(flagKey, 1, value, variation); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/FlagBuilder.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import com.launchdarkly.sdk.EvaluationReason; 6 | import com.launchdarkly.sdk.LDValue; 7 | import com.launchdarkly.sdk.android.DataModel.Flag; 8 | 9 | public final class FlagBuilder { 10 | @NonNull 11 | private String key; 12 | private LDValue value = null; 13 | private int version; 14 | private Integer flagVersion = null; 15 | private Integer variation = null; 16 | private boolean trackEvents = false; 17 | private boolean trackReason = false; 18 | private Long debugEventsUntilDate = null; 19 | private EvaluationReason reason = null; 20 | private String[] prerequisites = null; 21 | 22 | public FlagBuilder(@NonNull String key) { 23 | this.key = key; 24 | } 25 | 26 | public FlagBuilder value(LDValue value) { 27 | this.value = value; 28 | return this; 29 | } 30 | 31 | public FlagBuilder value(boolean boolValue) { 32 | return value(LDValue.of(boolValue)); 33 | } 34 | 35 | public FlagBuilder version(int version) { 36 | this.version = version; 37 | return this; 38 | } 39 | 40 | public FlagBuilder flagVersion(Integer flagVersion) { 41 | this.flagVersion = flagVersion; 42 | return this; 43 | } 44 | 45 | public FlagBuilder variation(Integer variation) { 46 | this.variation = variation; 47 | return this; 48 | } 49 | 50 | public FlagBuilder trackEvents(Boolean trackEvents) { 51 | this.trackEvents = trackEvents; 52 | return this; 53 | } 54 | 55 | public FlagBuilder trackReason(Boolean trackReason) { 56 | this.trackReason = trackReason; 57 | return this; 58 | } 59 | 60 | public FlagBuilder debugEventsUntilDate(Long debugEventsUntilDate) { 61 | this.debugEventsUntilDate = debugEventsUntilDate; 62 | return this; 63 | } 64 | 65 | public FlagBuilder reason(EvaluationReason reason) { 66 | this.reason = reason; 67 | return this; 68 | } 69 | 70 | public FlagBuilder prerequisites(String[] prerequisites) { 71 | this.prerequisites = prerequisites; 72 | return this; 73 | } 74 | 75 | public Flag build() { 76 | return new Flag(key, value, version, flagVersion, variation, trackEvents, trackReason, debugEventsUntilDate, reason, prerequisites); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/InMemoryPersistentDataStore.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.HashMap; 8 | import java.util.HashSet; 9 | import java.util.Map; 10 | 11 | /** 12 | * An implementation of {@link PersistentDataStore} that keeps data in a simple in-memory map and 13 | * does not write it to SharedPreferences. 14 | *

15 | * Currently this component is used only in testing, to ensure that tests do not interfere with each 16 | * other's state. In the future, we may provide a way for applications to choose a persistence 17 | * implementation. 18 | */ 19 | public final class InMemoryPersistentDataStore implements PersistentDataStore { 20 | private final Map> data = new HashMap<>(); 21 | 22 | @Override 23 | public synchronized String getValue(String storeNamespace, String key) { 24 | Map namespaceMap = data.get(storeNamespace); 25 | return namespaceMap == null ? null : namespaceMap.get(key); 26 | } 27 | 28 | @Override 29 | public synchronized void setValue(String storeNamespace, String key, String value) { 30 | Map namespaceMap = data.get(storeNamespace); 31 | if (namespaceMap == null) { 32 | namespaceMap = new HashMap<>(); 33 | data.put(storeNamespace, namespaceMap); 34 | } 35 | namespaceMap.put(key, value); 36 | } 37 | 38 | @Override 39 | public synchronized void setValues(String storeNamespace, Map keysAndValues) { 40 | Map namespaceMap = data.get(storeNamespace); 41 | if (namespaceMap == null) { 42 | namespaceMap = new HashMap<>(); 43 | data.put(storeNamespace, namespaceMap); 44 | } 45 | for (Map.Entry kv: keysAndValues.entrySet()) { 46 | if (kv.getValue() == null) { 47 | namespaceMap.remove(kv.getKey()); 48 | } else { 49 | namespaceMap.put(kv.getKey(), kv.getValue()); 50 | } 51 | } 52 | } 53 | 54 | @Override 55 | public synchronized Collection getKeys(String storeNamespace) { 56 | Map namespaceMap = data.get(storeNamespace); 57 | return namespaceMap == null ? Collections.emptyList() : 58 | new HashSet<>(namespaceMap.keySet()); 59 | } 60 | 61 | @Override 62 | public synchronized Collection getAllNamespaces() { 63 | return new HashSet<>(data.keySet()); 64 | } 65 | 66 | @Override 67 | public synchronized void clear(String storeNamespace, boolean fullyDelete) { 68 | data.remove(storeNamespace); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/LogCaptureRule.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static org.hamcrest.CoreMatchers.allOf; 4 | import static org.hamcrest.CoreMatchers.anything; 5 | import static org.hamcrest.CoreMatchers.containsString; 6 | import static org.hamcrest.CoreMatchers.hasItems; 7 | import static org.hamcrest.CoreMatchers.not; 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | 10 | import com.launchdarkly.logging.LDLogAdapter; 11 | import com.launchdarkly.logging.LDLogger; 12 | import com.launchdarkly.logging.LogCapture; 13 | import com.launchdarkly.logging.Logs; 14 | 15 | import org.junit.rules.TestWatcher; 16 | import org.junit.runner.Description; 17 | 18 | /** 19 | * Using this rule in a test class causes it to create a logger instance that captures output. 20 | * If the test fails, the output is dumped to the console so it will appear along with the test 21 | * failure output. If the test passes, the output is discarded. 22 | */ 23 | public final class LogCaptureRule extends TestWatcher { 24 | public LDLogger logger; 25 | public LDLogAdapter logAdapter; 26 | public LogCapture logCapture; 27 | 28 | public LogCaptureRule() { 29 | logCapture = Logs.capture(); 30 | logAdapter = logCapture; 31 | logger = LDLogger.withAdapter(logCapture, ""); 32 | } 33 | 34 | @Override 35 | protected void failed(Throwable e, Description description) { 36 | for (LogCapture.Message m: logCapture.getMessages()) { 37 | System.out.println("LOG >>> " + m.toStringWithTimestamp()); 38 | } 39 | } 40 | 41 | public void assertNothingLogged() { 42 | assertThat(logCapture.getMessages(), not(hasItems(anything()))); 43 | } 44 | 45 | public void assertNoErrorsLogged() { 46 | assertThat(logCapture.getMessageStrings(), not(hasItems(containsString("ERROR:")))); 47 | } 48 | 49 | public void assertNoWarningsLogged() { 50 | assertThat(logCapture.getMessageStrings(), not(hasItems(containsString("WARN:")))); 51 | } 52 | 53 | public void assertInfoLogged(String messageSubstring) { 54 | assertThat(logCapture.getMessageStrings(), 55 | hasItems(allOf(containsString("INFO:")), 56 | containsString(messageSubstring))); 57 | } 58 | 59 | public void assertWarnLogged(String messageSubstring) { 60 | assertThat(logCapture.getMessageStrings(), 61 | hasItems(allOf(containsString("WARN:")), 62 | containsString(messageSubstring))); 63 | } 64 | public void assertErrorLogged(String messageSubstring) { 65 | assertThat(logCapture.getMessageStrings(), 66 | hasItems(allOf(containsString("ERROR:")), 67 | containsString(messageSubstring))); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/MockPlatformState.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.util.concurrent.CopyOnWriteArrayList; 6 | 7 | /** 8 | * Test fixture implementation of {@link PlatformState} that is easier to manipulate in tests than 9 | * a regular mock. 10 | */ 11 | public class MockPlatformState implements PlatformState { 12 | private final CopyOnWriteArrayList connectivityChangeListeners = 13 | new CopyOnWriteArrayList<>(); 14 | private final CopyOnWriteArrayList foregroundChangeListeners = 15 | new CopyOnWriteArrayList<>(); 16 | 17 | private volatile boolean foreground = true; 18 | private volatile boolean networkAvailable = true; 19 | 20 | @Override 21 | public boolean isNetworkAvailable() { 22 | return networkAvailable; 23 | } 24 | 25 | public void setNetworkAvailable(boolean networkAvailable) { 26 | this.networkAvailable = networkAvailable; 27 | } 28 | 29 | @Override 30 | public void addConnectivityChangeListener(ConnectivityChangeListener listener) { 31 | connectivityChangeListeners.add(listener); 32 | } 33 | 34 | @Override 35 | public void removeConnectivityChangeListener(ConnectivityChangeListener listener) { 36 | connectivityChangeListeners.remove(listener); 37 | } 38 | 39 | public void notifyConnectivityChangeListeners(boolean networkAvailable) { 40 | new Thread(() -> { 41 | for (ConnectivityChangeListener listener: connectivityChangeListeners) { 42 | listener.onConnectivityChanged(networkAvailable); 43 | } 44 | }).start(); 45 | } 46 | 47 | @Override 48 | public boolean isForeground() { 49 | return foreground; 50 | } 51 | 52 | public void setForeground(boolean foreground) { 53 | this.foreground = foreground; 54 | } 55 | 56 | @Override 57 | public void addForegroundChangeListener(ForegroundChangeListener listener) { 58 | foregroundChangeListeners.add(listener); 59 | } 60 | 61 | @Override 62 | public void removeForegroundChangeListener(ForegroundChangeListener listener) { 63 | foregroundChangeListeners.remove(listener); 64 | } 65 | 66 | public void setAndNotifyForegroundChangeListeners(boolean foreground) { 67 | this.foreground = foreground; 68 | new Thread(() -> { 69 | for (ForegroundChangeListener listener: foregroundChangeListeners) { 70 | listener.onForegroundChanged(foreground); 71 | } 72 | }).start(); 73 | } 74 | 75 | @Override 76 | public File getCacheDir() { 77 | return null; 78 | } 79 | 80 | @Override 81 | public void close() throws IOException {} 82 | } 83 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/NullPersistentDataStore.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; 4 | 5 | import java.util.Collection; 6 | import java.util.Collections; 7 | import java.util.Map; 8 | 9 | public final class NullPersistentDataStore implements PersistentDataStore { 10 | @Override 11 | public String getValue(String storeNamespace, String key) { 12 | return null; 13 | } 14 | 15 | @Override 16 | public void setValue(String storeNamespace, String key, String value) {} 17 | 18 | @Override 19 | public void setValues(String storeNamespace, Map keysAndValues) {} 20 | 21 | @Override 22 | public Collection getKeys(String storeNamespace) { 23 | return Collections.emptyList(); 24 | } 25 | 26 | @Override 27 | public Collection getAllNamespaces() { 28 | return Collections.emptyList(); 29 | } 30 | 31 | @Override 32 | public void clear(String storeNamespace, boolean fullyDelete) {} 33 | } 34 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/SimpleTestTaskExecutor.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import java.util.concurrent.Executors; 4 | import java.util.concurrent.ScheduledExecutorService; 5 | import java.util.concurrent.ScheduledFuture; 6 | import java.util.concurrent.TimeUnit; 7 | 8 | /** 9 | * An implementation of {@link TaskExecutor} that can be used in unit tests outside of the Android 10 | * environment. This allows us to unit-test components separately from the implementation details of 11 | * how threads are managed in Android, verifying only that those components are calling the correct 12 | * {@link TaskExecutor} methods. 13 | */ 14 | public class SimpleTestTaskExecutor implements TaskExecutor { 15 | private static final ThreadLocal fakeMainThread = new ThreadLocal<>(); 16 | 17 | private final ScheduledExecutorService executor = Executors.newSingleThreadScheduledExecutor(); 18 | 19 | @Override 20 | public void executeOnMainThread(Runnable action) { 21 | new Thread(() -> { 22 | fakeMainThread.set(Thread.currentThread()); 23 | action.run(); 24 | }).start(); 25 | } 26 | 27 | @Override 28 | public ScheduledFuture scheduleTask(Runnable action, long delayMillis) { 29 | return executor.schedule(action, delayMillis, TimeUnit.MILLISECONDS); 30 | } 31 | 32 | @Override 33 | public ScheduledFuture startRepeatingTask(Runnable action, long initialDelayMillis, long intervalMillis) { 34 | return executor.scheduleAtFixedRate(action, 35 | initialDelayMillis, intervalMillis, TimeUnit.MILLISECONDS); 36 | } 37 | 38 | @Override 39 | public void close() { 40 | executor.shutdownNow(); 41 | } 42 | 43 | public boolean isThisTheFakeMainThread() { 44 | return fakeMainThread.get() == Thread.currentThread(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /shared-test-code/src/main/java/com/launchdarkly/sdk/android/TestUtil.java: -------------------------------------------------------------------------------- 1 | package com.launchdarkly.sdk.android; 2 | 3 | import static org.junit.Assert.assertNotNull; 4 | import static org.junit.Assert.assertNull; 5 | import static org.junit.Assert.fail; 6 | 7 | import com.launchdarkly.logging.LDLogger; 8 | import com.launchdarkly.sdk.LDContext; 9 | import com.launchdarkly.sdk.android.DataModel.Flag; 10 | import com.launchdarkly.sdk.android.subsystems.PersistentDataStore; 11 | 12 | import java.util.concurrent.BlockingQueue; 13 | import java.util.concurrent.TimeUnit; 14 | import java.util.concurrent.atomic.AtomicReference; 15 | 16 | public class TestUtil { 17 | public static T requireValue(BlockingQueue queue, long timeout, TimeUnit timeoutUnit, String description) { 18 | try { 19 | T value = queue.poll(timeout, timeoutUnit); 20 | assertNotNull("timed out waiting for " + description, value); 21 | return value; 22 | } catch (InterruptedException e) { 23 | throw new RuntimeException(e); 24 | } 25 | } 26 | 27 | public static void requireNoMoreValues(BlockingQueue queue, long timeout, TimeUnit timeoutUnit, String description) { 28 | try { 29 | T value = queue.poll(timeout, timeoutUnit); 30 | assertNull("received unexpected " + description, value); 31 | } catch (InterruptedException e) { 32 | throw new RuntimeException(e); 33 | } 34 | } 35 | 36 | public static PersistentDataStoreWrapper makeSimplePersistentDataStoreWrapper() { 37 | return new PersistentDataStoreWrapper( 38 | new InMemoryPersistentDataStore(), 39 | LDLogger.none() 40 | ); 41 | } 42 | 43 | public static void writeFlagUpdateToStore( 44 | PersistentDataStore store, 45 | String mobileKey, 46 | LDContext context, 47 | Flag flag 48 | ) { 49 | PersistentDataStoreWrapper.PerEnvironmentData environmentStore = 50 | new PersistentDataStoreWrapper(store, LDLogger.none()).perEnvironmentData(mobileKey); 51 | EnvironmentData data = environmentStore.getContextData(LDUtil.urlSafeBase64HashedContextId(context)); 52 | EnvironmentData newData = (data == null ? new EnvironmentData() : data).withFlagUpdatedOrAdded(flag); 53 | environmentStore.setContextData(LDUtil.urlSafeBase64HashedContextId(context), LDUtil.urlSafeBase64Hash(context), newData); 54 | } 55 | 56 | public static void doSynchronouslyOnNewThread(Runnable action) { 57 | // This is a workaround for Android's prohibition on doing certain network operations on 58 | // the main thread-- even in tests. There is *supposed* to be a way around this with 59 | // `StrictMode#setThreadPolicy` but we've had trouble using it in emulators. 60 | try { 61 | AtomicReference thrown = new AtomicReference<>(); 62 | Thread t = new Thread(() -> { 63 | try { 64 | action.run(); 65 | } catch (RuntimeException e) { 66 | thrown.set(e); 67 | } 68 | }); 69 | t.start(); 70 | t.join(); 71 | if (thrown.get() != null) { 72 | throw thrown.get(); 73 | } 74 | } catch (InterruptedException err) { 75 | fail("failed to run thread"); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /testharness-suppressions.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/launchdarkly/android-client-sdk/e5c6daffa2c9fa5607dd053af6aecc40c0cbc7e0/testharness-suppressions.txt --------------------------------------------------------------------------------