diff --git a/.circleci/config.yml b/.circleci/config.yml index 0cdaaaee..69c33ca6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,60 +1,53 @@ -version: 2 +version: 2.1 +orbs: + android: circleci/android@1.0 jobs: build: - working_directory: ~/launchdarkly/android-client-sdk-private - macos: - xcode: "10.3.0" - shell: /bin/bash --login -eo pipefail - environment: - TERM: dumb - QEMU_AUDIO_DRV: none - _JAVA_OPTIONS: "-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -Xms2048m -Xmx4096m" - GRADLE_OPTS: "-Dorg.gradle.daemon=false" - JVM_OPTS: -Xmx3200m - ANDROID_HOME: "/usr/local/share/android-sdk" - ANDROID_SDK_HOME: "/usr/local/share/android-sdk" - ANDROID_SDK_ROOT: "/usr/local/share/android-sdk" + executor: + name: android/android-machine + resource-class: large steps: - checkout - - run: - name: Setup env - command: | - echo 'export PATH="$PATH:/usr/local/share/android-sdk/tools/bin"' >> $BASH_ENV - echo 'export PATH="$PATH:/usr/local/share/android-sdk/platform-tools"' >> $BASH_ENV - - run: - name: Install Android sdk - command: | - HOMEBREW_NO_AUTO_UPDATE=1 brew tap homebrew/cask - HOMEBREW_NO_AUTO_UPDATE=1 brew cask install android-sdk - - run: sudo mkdir -p /usr/local/android-sdk-linux/licenses - - run: - name: Install emulator dependencies - command: yes | sdkmanager "platform-tools" "platforms;android-25" "extras;intel;Hardware_Accelerated_Execution_Manager" "build-tools;26.0.2" "system-images;android-25;default;x86" "emulator" | grep -v = || true - - run: | - set +o pipefail - yes | sdkmanager --licenses - - run: - name: Download Dependencies - command: ./gradlew androidDependencies - - run: unset ANDROID_NDK_HOME + # Create and start emulator + - android/create-avd: + avd-name: ci-android-avd + system-image: system-images;android-25;default;x86 + install: true + + - android/start-emulator: + avd-name: ci-android-avd + wait-for-emulator: false + + # Perform tasks we can do while waiting for emulator to start + + # Restore caches + - android/restore-gradle-cache + - android/restore-build-cache - - run: echo no | avdmanager create avd -n ci-android-avd -f -k "system-images;android-25;default;x86" - run: - command: $ANDROID_HOME/emulator/emulator -avd ci-android-avd -netdelay none -netspeed full -no-audio -no-window -no-snapshot -no-boot-anim - background: true - timeout: 1200 - no_output_timeout: 2h + name: Build local tests + command: ./gradlew :launchdarkly-android-client-sdk:assembleDebugUnitTest - run: - name: Compile - command: ./gradlew :launchdarkly-android-client-sdk:assembleDebug --console=plain -PdisablePreDex + name: Build connected tests + command: ./gradlew :launchdarkly-android-client-sdk:assembleDebugAndroidTest + # Save caches + - android/save-build-cache + - android/save-gradle-cache + + # Run unit tests that do not require the emulator - run: - name: Wait for emulator to boot - command: .circleci/scripts/circle-android wait-for-boot + name: Run local tests + command: ./gradlew :launchdarkly-android-client-sdk:testDebugUnitTest + + # Now wait for emulator to fully start + - android/wait-for-emulator + # Additional validation that emulator is fully started and will accept adb + # commands - run: name: Validate retrieving emulator SDK version command: | @@ -62,6 +55,8 @@ jobs: sleep 1 done + # Necessary for test mocking to disable network access through WiFi + # configuration, allowing testing of behavior when device is offline - run: name: Disable mobile data for network tests command: adb shell svc data disable @@ -74,15 +69,10 @@ jobs: adb shell getprop | tee -a ~/artifacts/props.txt adb logcat | tee -a ~/artifacts/logcat.txt - - run: - name: Run tests - command: ./gradlew :launchdarkly-android-client-sdk:connectedAndroidTest --console=plain -PdisablePreDex - no_output_timeout: 2h + - android/run-tests: + max-tries: 1 - - run: - name: Stop emulator - command: adb emu kill || true - when: always + - android/kill-emulators - run: name: Validate package creation @@ -95,16 +85,18 @@ jobs: - run: name: Save test results command: | - mkdir -p ~/test-results - cp -r ./launchdarkly-android-client-sdk/build/outputs/androidTest-results/* ~/test-results/ + mkdir -p ~/test-results + cp -r ./launchdarkly-android-client-sdk/build/test-results/testDebugUnitTest ~/test-results/ + cp -r ./launchdarkly-android-client-sdk/build/outputs/androidTest-results/* ~/test-results/ when: always - run: name: Save artifacts command: | - mv ./launchdarkly-android-client-sdk/build/reports ~/artifacts - mv ./launchdarkly-android-client-sdk/build/outputs ~/artifacts - mv ./launchdarkly-android-client-sdk/build/docs ~/artifacts + mv ./launchdarkly-android-client-sdk/build/test-results ~/artifacts + mv ./launchdarkly-android-client-sdk/build/reports ~/artifacts + mv ./launchdarkly-android-client-sdk/build/outputs ~/artifacts + mv ./launchdarkly-android-client-sdk/build/docs ~/artifacts when: always - store_test_results: diff --git a/.circleci/scripts/LICENSE b/.circleci/scripts/LICENSE deleted file mode 100644 index 13115564..00000000 --- a/.circleci/scripts/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2017-2019 Circle Internet Services, Inc. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/.circleci/scripts/circle-android b/.circleci/scripts/circle-android deleted file mode 100755 index 4524b60b..00000000 --- a/.circleci/scripts/circle-android +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env python - -# See LICENSE file in this directory for copyright and license information -# Above LICENSE notice added 10/16/2019 by Gavin Whelan - -from sys import argv, exit, stdout -from time import sleep, time -from os import system -from subprocess import check_output, CalledProcessError -from threading import Thread, Event -from functools import partial - -class StoppableThread(Thread): - - def __init__(self): - super(StoppableThread, self).__init__() - self._stop_event = Event() - self.daemon = True - - def stopped(self): - return self._stop_event.is_set() - - def run(self): - while not self.stopped(): - stdout.write('.') - stdout.flush() - sleep(2) - - def stop(self): - self._stop_event.set() - -def shell_getprop(name): - try: - return check_output(['adb', 'shell', 'getprop', name]).strip() - except CalledProcessError as e: - return '' - -start_time = time() - -def wait_for(name, fn): - stdout.write('Waiting for %s' % name) - spinner = StoppableThread() - spinner.start() - stdout.flush() - while True: - if fn(): - spinner.stop() - time_taken = int(time() - start_time) - print('\n%s is ready after %d seconds' % (name, time_taken)) - break - sleep(1) - -def device_ready(): - return system('adb wait-for-device') == 0 - -def shell_ready(): - return system('adb shell true &> /dev/null') == 0 - -def prop_has_value(prop, value): - return shell_getprop(prop) == value - -def wait_for_sys_prop(name, prop, value): - # return shell_getprop('init.svc.bootanim') == 'stopped' - wait_for(name, partial(prop_has_value, prop, value)) - -usage = """ -%s, a collection of tools for CI with android. - -Usage: - %s wait-for-boot - wait for a device to fully boot. - (adb wait-for-device only waits for it to be ready for shell access). -""" - -if __name__ == "__main__": - - if len(argv) != 2 or argv[1] != 'wait-for-boot': - print(usage % (argv[0], argv[0])) - exit(0) - - wait_for('Device', device_ready) - wait_for('Shell', shell_ready) - wait_for_sys_prop('Boot animation complete', 'init.svc.bootanim', 'stopped') - wait_for_sys_prop('Boot animation exited', 'service.bootanim.exit', '1') - wait_for_sys_prop('System boot complete', 'sys.boot_completed', '1') - wait_for_sys_prop('GSM Ready', 'gsm.sim.state', 'READY') - #wait_for_sys_prop('init.svc.clear-bcb' ,'init.svc.clear-bcb', 'stopped') - - diff --git a/.gitignore b/.gitignore index d6506546..92e13233 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,12 @@ .DS_Store build /captures - +.project *.iml .idea -.idea/* \ No newline at end of file +.idea/* +.classpath +.project +.settings/ +.vscode/ diff --git a/CHANGELOG.md b/CHANGELOG.md index c030a81f..24a66912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,86 @@ All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [3.0.0] - 2021-05-07 +This major version has an accompanying [Migration Guide](https://docs.launchdarkly.com/sdk/client-side/android/migration-2-to-3). Please see the guide for more information on updating to this version of the SDK, as the following is just a summary of the changes. + +Usages of `Gson` provided types have been removed from the public API, replacing `JsonElement` with `LDValue` provided by the SDK. `LDValue` can represent the same values as a `JsonElement`, but has a diferent API. See the [API documentation](https://launchdarkly.github.io/android-client-sdk/com/launchdarkly/sdk/LDValue.html) for a detailed reference. +### Added +- `LDConfig.Builder` customization: + * The `autoAliasingOptOut` configuration option that is used to control the new automatic aliasing behavior of the `identify` method; by setting `autoAliasingOptOut` to true, `identify` will not automatically generate alias events. + * The `headerTransform` configuration option that supersedes the previous `additionalHeaders` configuration option by allowing fully dynamic updating of headers for requests the SDK makes to the LaunchDarkly service. + * The `privateAttributes` configuration option that replaces `setPrivateAttributeNames`, specifying the private attributes as vararg `UserAttribute` arguments rather than a `Set`. This allows easily specifying built-in attributes. +- `LDUser(String)` constructor that creates a fully default user. +- New accessors for `LDUser` + * `getAttribute(UserAttribute)` for programmatically retrieving attribute values. + * `getCustomAttributes()` for retrieving the currently set custom attributes. + * `getPrivateAttributes()` for retrieving the attributes set to be private on this user. + * `isAttributePrivate(UserAttribute)` for checking if a given attribute is private. + * Getters for all built-in attributes, e.g. `getName()` +- New `LDUser.Builder` methods overloads for `custom` and `privateCustom`: + * `custom(String, boolean)` and `privateCustom(String, boolean)` for setting custom attributes to boolean values. + * `custom(String, int)`, `privateCustom(String, int)`, `custom(String, double)`, and `privateCustom(String, double)` for setting custom attributes to numeric values. + * `custom(String, LDValue)` and `privateCustom(String, LDValue)` for setting custom attributes to arbitrary data. +- The `UserAttribute` class, which provides a less error-prone way to refer to user attribute names in configuration. This class can also be used to get arbitrary attribute- `LDClient` functionality: + * The `alias` method that is used to associate two user objects for analytics purposes with an alias event. + * `jsonValueVariation` and `jsonValueVariationDetail`. These are equivalent to the removed `jsonVariation` and `jsonVariationDetail` other than using `LDValue` instead of `JsonElement`. + * `trackData(String, LDValue)` which replaces `track(String, JsonElement)`. Other than changing to use `LDValue` the behavior is the same. + * `trackMetric(String, LDValue, double)` which replaces `track(String, JsonElement, Double)`. This also uses `LDValue` rather than `JsonElement`, and requires a metric value. Otherwise use `trackData`. +- The `LDGson` and `LDJackson` classes, which allow SDK classes like `LDUser` to be easily converted to or from JSON using the popular Gson and Jackson frameworks. +- `EvaluationDetail.fromValue` and `EvaluationDetail.error` factory methods. +- `LDHeaderUpdater` interface for the new `headerTransform` configuration option. +### Fixed +- Fixed an issue where the SDK could log error level messages when attempting to send diagnostic events without an internet connection. The SDK will no longer attempt to send diagnostic events when an internet connection is known to be unavailable, and will not log an error level message if the connection fails. Thanks to @valeriyo for reporting ([#107](https://github.com/launchdarkly/android-client-sdk/issues/107)). +- Fixed an issue where `LDUser` instances created before calling `LDClient.init` without specifying a key would have the key `UNKNOWN_ANDROID` rather than a device unique key. +- Fixed an issue where flags listeners would be informed of changes to unchanged flags whenever the SDK receives an entire flag set (on a new stream connection, a poll request, or any stream updates behind a relay proxy). +- Fixed an issue where a `NullPointerException` is thrown if `LDClient.close()` is called multiple times. +- Improved the proguard/R8 configuration to allow more optimization. Thanks to @valeriyo for requesting ([#106](https://github.com/launchdarkly/android-client-sdk/issues/106)) +- Fixed a potential issue where the SDK could cause additional throttling on requests to the backend service when previously throttled requests had been cancelled before completion. +### Changed (requirements/dependencies/build) +- Migrated from using the Android Support Libraries to using AndroidX from Jetpack. Using AndroidX requires the `android.useAndroidX` Android Gradle plugin flag to be set to `true` in your application's `gradle.properties` file. If your application previously set the `android.enableJetifier` Android Gradle plugin flag to `true` in it's `gradle.properties` file soley for the LaunchDarkly SDK, this flag can now be removed. Thanks to everyone who requested this enhancement ([#103](https://github.com/launchdarkly/android-client-sdk/issues/103)). +- The minimum Android API version has been raised from API level 16 (Android 4.1 Jelly Bean) to API level 21 (Android 5.0 Lollipop). +- The SDK no longer has a dependency on Google Play Services. This dependency was only used on pre-21 Android API levels to improve TLS 1.2 compatibility, as the minimum Android version has been raised to 21, the dependency is no longer necessary. +- The SDK is now built with modern Gradle (6.7, Android plugin 4.1.3) and uses Java 8. +### Changed (API) +- Package names have changed: the main SDK classes are now in `com.launchdarkly.sdk` and `com.launchdarkly.sdk.android`. +- All `LDConfig.Builder` setters have been renamed to remove the `set` prefix, e.g. `LDConfig.Builder.setMobileKey` has been renamed to `LDConfig.Builder.mobileKey`. +- `LDClient` API changes: + * `boolVariation` and `intVariation` no longer use nullable object types for argument and return values, instead using primitive types, e.g. `Boolean boolVariation(String, Boolean)` became `boolean boolVariation(String, boolean)`. + * `boolVariationDetail` and `intVariationDetail` no longer use nullable object types for argument values, instead using primitive types, e.g. `boolVariationDetail(String, Boolean)` became `boolVariationDetail(String, boolean)`. + * `floatVariation` and `floatVariationDetail` have been changed to have the same behavior as the removed `doubleVariation` and `doubleVariationDetail`. + * `allFlags()` now returns `Map` rather than `Map`. Rather than the returned `Map` containing `Boolean`, `Float`, and `String` typed objects, with JSON values represented as strings, the `Map` contains `LDValue` typed objects which return the source type (including complex types such as JSON arrays and objects). +- `EvaluationDetail.getVariationIndex()` now returns `int` instead of `Integer`. No variation index is now represented as the constant `EvaluationReason.NO_VARIATION`. +- `EvaluationReason` is now a single concrete class rather than an abstract base class. Usages of the sub-classes can be replaced with the base class. +### Changed (behavioral) +- The default polling domain (configurable with `LDConfig.Builder.pollUri`) has changed from `app.launchdarkly.com` to `clientsdk.launchdarkly.com`. +- The default `eventsUri` used to send events to the service has changed from `https://mobile.launchdarkly.com/mobile` to `https://mobile.launchdarkly.com`. The SDK will now append the expected endpoint path (`/mobile`) to the configured `Uri`, which is more consistent with other LaunchDarkly SDKs. +- For compatibility with older SDK behavior, the `LDClient.stringVariation` method could be used to retrieve JSON flags in a serialized representation. This compatibility behavior has been removed, and attempts to request a JSON valued flag using `stringVariation` will behave the same as other mismatched type variation calls. +- The `LDClient.identify` method will now automatically generate an alias event when switching from an anonymous to a known user. This event associates the two users for analytics purposes as they most likely represent a single person. This behavior can be disabled with the `autoAliasingOptOut` configuration option. +- All log messages are now tagged `LaunchDarklySdk` for easier filtering. Thanks to @valeriyo for the suggestion ([#113](https://github.com/launchdarkly/android-client-sdk/issues/113)). +- `LDUser` now overrides `equals`, `hashCode`, and `toString` with appropriate implementations. +- `LDUser.Builder.country(String)` and `LDUser.Builder.privateCountry(String)` no longer attempt to look up the country from the provided `String` (attempting to match it as an ISO-3166-1 alpha-2, alpha-3 code; or a country name) and set the country to the resultant IOS-3166-1 alpha-2 only if successful. The SDK no longer gives this attribute special behavior, and sets the user's country attribute directly as the provided `String`. +### Removed +- `LDConfig.Builder`: + * `setBaseUri(Uri)` has been removed. Please use `setPollUri(Uri)` instead. + * `setAdditionalHeaders(Map)` has been removed. Please use `headerTransform(LDHeaderUpdater)` instead. + * `setPrivateAttributeNames(Set)` has been removed. Please use `privateAttributes(UserAttribute...)` instead. +- `LDUser.Builder`: + * `country(LDCountryCode)` and `privateCountry(LDCountryCode)` have been removed. Use `country(String)` or `privateCountry(String)` to set the country value on a user. + * `custom(String, Number)` and `privateCustom(String, Number)` have been removed. Use the `(String, int)` or `(String, double)` overloads instead. + * `custom(String, Boolean)` and `privateCustom(String, Boolean)` have been removed. Use `custom(String, boolean)` or `privateCustom(String, boolean)` instead. + * `custom(String, List)`, `LDUser.customString(String, List)`, `LDUser.privateCustomString(String, List)`. Use `custom(String, LDValue)` and `privateCustom(String, LDValue)` instead. + * `customNumber(String, List)` and `LDUser.privateCustomNumber(String, List)`. Use `custom(String, LDValue)` and `privateCustom(String, LDValue)` instead. +- `LDClient`: + * `doubleVariation` and `doubleVariationDetail` have been removed. Use `floatVariation` and `floatVariationDetail` instead. + * `jsonVariation` and `jsonVariationDetail` have been removed. Use `jsonValueVariation` and `jsonValueVariationDetail` instead. + * `track(String, JsonElement)` and `track(String, JsonElement, Double)` overloads have been removed, please use the designated methods `trackData(String, LDValue)` and `trackMetric(String, LDValue, double)` instead. +- The public constructor for `EvaluationDetail` has been hidden. Use the new factory methods `EvaluationDetail.fromValue` and `EvaluationDetail.error` instead. +- The concrete sub-classes of `EvaluationReason` have been removed in favor of making `EvaluationReason` a concrete class. The accessors on the sub-classes have been moved to the base class. Instead of using `instanceOf` to determine the type, use `getKind()`. +- `LDCountryCode` has been removed as no SDK APIs use this class. +- All classes and interfaces in the `com.launchdarkly.sdk.android.flagstore`, `com.launchdarkly.sdk.android.gson`, `com.launchdarkly.sdk.android.response`, and `com.launchdarkly.sdk.android.tls` packages. These classes and interfaces were not intended for external use. +- `Debounce`, `FeatureFlagFetcher`, `SummaryEventSharedPreferences`, `UserSummaryEventSharedPreferences`, and `Util` in `com.launchdarkly.sdk.android`. These deprecated classes and interfaces were not intended for external use. + + ## [2.14.1] - 2021-01-14 ### Fixed - Before this release, the SDK could cause an uncaught exception on certain Android implementations, when scheduling a future poll request under certain situations. This fix extends a previous fix implemented in the [2.9.1 release](https://github.com/launchdarkly/android-client-sdk/releases/tag/2.9.1) of the SDK, which catches `SecurityException`s thrown by the alarm manager when registering an alarm for the next poll. This `SecurityException` was introduced by Samsung on their Lollipop and later Android implementions, and is thrown when the application has at least 500 existing alarms when registering a new alarm. After recent reports of the alarm manager throwing an `IllegalStateException` rather than a `SecurityException` under the same conditions but different Android implementations, this release broadens the exception handling when scheduling a poll request to safeguard against other exception types. diff --git a/README.md b/README.md index 3e623ce3..6a6031fe 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## Supported Android versions -This version of the LaunchDarkly SDK has been tested with Android SDK versions 16 and up (4.1 Jelly Bean). +This version of the LaunchDarkly SDK has been tested with Android SDK versions 21 and up (5.0 Lollipop). ## Getting started @@ -41,4 +41,3 @@ We encourage pull requests and other contributions from the community. Check out * [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation * [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates - * [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies \ No newline at end of file diff --git a/build.gradle b/build.gradle index 608281a1..1ae71ae1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,43 +1,73 @@ +import java.time.Duration + // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { repositories { mavenCentral() google() - jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.0.1' + classpath("com.android.tools.build:gradle:4.1.3") // For displaying method/field counts when building with Gradle: // https://github.com/KeepSafe/dexcount-gradle-plugin - classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2' + classpath("com.getkeepsafe.dexcount:dexcount-gradle-plugin:2.0.0") // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } +plugins { + id("java") + id("io.github.gradle-nexus.publish-plugin") version "1.1.0" +} + +// Must be specified in root project for the gradle nexus publish plugin. +group = "com.launchdarkly" +// Specified in gradle.properties +version = version + allprojects { repositories { - mavenCentral() + mavenLocal() + // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: + maven { url = uri("https://oss.sonatype.org/content/groups/public/") } google() - jcenter() + mavenCentral() } } -task clean(type: Delete) { - delete rootProject.buildDir -} - -project.ext.preDexLibs = !project.hasProperty('disablePreDex') - subprojects { - project.plugins.whenPluginAdded { plugin -> - if ("com.android.build.gradle.AppPlugin".equals(plugin.class.name)) { - project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs - } else if ("com.android.build.gradle.LibraryPlugin".equals(plugin.class.name)) { - project.android.dexOptions.preDexLibraries = rootProject.ext.preDexLibs + afterEvaluate { + configure(android.lintOptions) { + abortOnError = false + } + configure(android.compileOptions) { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + gradle.projectsEvaluated { + tasks.withType(JavaCompile) { + // enable deprecation checks + options.compilerArgs << "-Xlint:deprecation" + } } } } + +nexusPublishing { + packageGroup = "com.launchdarkly" + repositories { + sonatype { + nexusUrl.set(uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/")) + snapshotRepositoryUrl.set(uri("https://oss.sonatype.org/content/repositories/snapshots/")) + } + } + + transitionCheckOptions { + maxRetries.set(20) + delayBetween.set(Duration.ofMillis(3000)) + } +} \ No newline at end of file diff --git a/example/build.gradle b/example/build.gradle index cdb57eb2..2b98a00a 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -1,54 +1,33 @@ -apply plugin: 'com.android.application' -// make sure this line comes *after* you apply the Android plugin -apply plugin: 'com.getkeepsafe.dexcount' - -repositories { - mavenLocal() - // Before LaunchDarkly release artifacts get synced to Maven Central they are here along with snapshots: - maven { url "https://oss.sonatype.org/content/groups/public/" } - google() - mavenCentral() +plugins { + id("com.android.application") + // make sure this line comes *after* you apply the Android plugin + id("com.getkeepsafe.dexcount") } android { - compileSdkVersion 26 - buildToolsVersion '26.0.2' + compileSdkVersion(30) + buildToolsVersion = "30.0.3" + defaultConfig { - applicationId "com.launchdarkly.example" - minSdkVersion 16 - targetSdkVersion 26 - versionCode 1 - versionName "1.0" + applicationId = "com.launchdarkly.example" + minSdkVersion(21) + targetSdkVersion(30) + versionCode = 1 + versionName = "1.0" } + buildTypes { release { - minifyEnabled true + minifyEnabled = true } } - lintOptions { - abortOnError false - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - - dexOptions { - javaMaxHeapSize "4g" - } - } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.google.code.gson:gson:2.8.2' - implementation 'com.android.support:appcompat-v7:26.1.0' - implementation project(path: ':launchdarkly-android-client-sdk') - // Comment the previous line and uncomment this one to depend on the published artifact: - //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.14.1' - - implementation 'com.jakewharton.timber:timber:4.7.1' + implementation("androidx.appcompat:appcompat:1.2.0") + implementation("com.jakewharton.timber:timber:4.7.1") - testImplementation 'junit:junit:4.12' + implementation(project(":launchdarkly-android-client-sdk")) + // Comment the previous line and uncomment this one to depend on the published artifact: + //implementation("com.launchdarkly:launchdarkly-android-client-sdk:3.0.0") } diff --git a/example/src/androidTest/java/com/launchdarkly/example/ApplicationTest.java b/example/src/androidTest/java/com/launchdarkly/example/ApplicationTest.java deleted file mode 100644 index b6f37c54..00000000 --- a/example/src/androidTest/java/com/launchdarkly/example/ApplicationTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.launchdarkly.example; - -import android.app.Application; -import android.test.ApplicationTestCase; - -/** - * Testing Fundamentals - */ -public class ApplicationTest extends ApplicationTestCase { - public ApplicationTest() { - super(Application.class); - } -} \ No newline at end of file diff --git a/example/src/main/AndroidManifest.xml b/example/src/main/AndroidManifest.xml index 8beb5d60..2d7d67da 100644 --- a/example/src/main/AndroidManifest.xml +++ b/example/src/main/AndroidManifest.xml @@ -2,8 +2,10 @@ - + + + + diff --git a/example/src/main/java/com/launchdarkly/example/MainActivity.java b/example/src/main/java/com/launchdarkly/example/MainActivity.java index c314a334..e3b7a663 100644 --- a/example/src/main/java/com/launchdarkly/example/MainActivity.java +++ b/example/src/main/java/com/launchdarkly/example/MainActivity.java @@ -3,30 +3,25 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; -import android.support.annotation.Nullable; -import android.support.v7.app.AppCompatActivity; -import android.view.View; import android.widget.ArrayAdapter; import android.widget.Button; -import android.widget.CompoundButton; import android.widget.EditText; import android.widget.Spinner; import android.widget.Switch; import android.widget.TextView; import android.widget.Toast; -import com.google.gson.JsonNull; -import com.launchdarkly.android.LDAllFlagsListener; -import com.launchdarkly.android.ConnectionInformation; -import com.launchdarkly.android.FeatureFlagChangeListener; -import com.launchdarkly.android.LDClient; -import com.launchdarkly.android.LDConfig; -import com.launchdarkly.android.LDFailure; -import com.launchdarkly.android.LDStatusListener; -import com.launchdarkly.android.LDUser; +import androidx.appcompat.app.AppCompatActivity; + +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.android.ConnectionInformation; +import com.launchdarkly.sdk.android.LDAllFlagsListener; +import com.launchdarkly.sdk.android.LDClient; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDFailure; +import com.launchdarkly.sdk.android.LDStatusListener; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -43,12 +38,7 @@ public class MainActivity extends AppCompatActivity { private void updateStatusString(final ConnectionInformation connectionInformation) { if (Looper.myLooper() != MainActivity.this.getMainLooper()) { - (new Handler(MainActivity.this.getMainLooper())).post(new Runnable() { - @Override - public void run() { - updateStatusString(connectionInformation); - } - }); + new Handler(MainActivity.this.getMainLooper()).post(() -> updateStatusString(connectionInformation)); } else { TextView connection = MainActivity.this.findViewById(R.id.connection_status); Long lastSuccess = connectionInformation.getLastSuccessfulConnection(); @@ -66,7 +56,7 @@ public void run() { } @Override - protected void onCreate(Bundle savedInstanceState) { + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); @@ -78,8 +68,8 @@ protected void onCreate(Bundle savedInstanceState) { setupListeners(); LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("MOBILE_KEY") - .setUseReport(false) // change to `true` if the request is to be REPORT'ed instead of GET'ed + .mobileKey("MOBILE_KEY") + .useReport(false) // change to `true` if the request is to be REPORT'ed instead of GET'ed .build(); LDUser user = new LDUser.Builder("user key") @@ -106,128 +96,74 @@ public void onConnectionModeChanged(final ConnectionInformation connectionInform @Override public void onInternalFailure(final LDFailure ldFailure) { - new Handler(MainActivity.this.getMainLooper()).post(new Runnable() { - @Override - public void run() { - Toast.makeText(MainActivity.this, ldFailure.toString(), Toast.LENGTH_SHORT).show(); - } + new Handler(MainActivity.this.getMainLooper()).post(() -> { + Toast.makeText(MainActivity.this, ldFailure.toString(), Toast.LENGTH_SHORT).show(); }); updateStatusString(ldClient.getConnectionInformation()); } }; - allFlagsListener = new LDAllFlagsListener() { - @Override - public void onChange(final List flagKey) { - new Handler(MainActivity.this.getMainLooper()).post(new Runnable() { - @Override - public void run() { - StringBuilder flags = new StringBuilder("Updated flags: "); - for (String flag : flagKey) { - flags.append(flag).append(" "); - } - Toast.makeText(MainActivity.this, flags.toString(), Toast.LENGTH_SHORT).show(); - } - }); - updateStatusString(ldClient.getConnectionInformation()); - } + allFlagsListener = flagKey -> { + new Handler(MainActivity.this.getMainLooper()).post(() -> { + StringBuilder flags = new StringBuilder("Updated flags: "); + for (String flag : flagKey) { + flags.append(flag).append(" "); + } + Toast.makeText(MainActivity.this, flags.toString(), Toast.LENGTH_SHORT).show(); + }); + updateStatusString(ldClient.getConnectionInformation()); }; } private void setupFlushButton() { Button flushButton = findViewById(R.id.flush_button); - flushButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Timber.i("flush onClick"); - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.flush(); - } - }); - } + flushButton.setOnClickListener(v -> { + Timber.i("flush onClick"); + MainActivity.this.doSafeClientAction(() -> ldClient.flush()); }); } - private interface LDClientFunction { + private interface LDClientAction { void call(); } - private interface LDClientGetFunction { - V get(); - } - - private void doSafeClientAction(LDClientFunction function) { + private void doSafeClientAction(LDClientAction function) { if (ldClient != null) { function.call(); } } - @Nullable - private V doSafeClientGet(LDClientGetFunction function) { - if (ldClient != null) { - return function.get(); - } - return null; + private interface LDClientGet { + V get(); + } + + private V doSafeClientGet(LDClientGet function) { + return ldClient != null ? function.get() : null; } private void setupTrackButton() { Button trackButton = findViewById(R.id.track_button); - trackButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Timber.i("track onClick"); - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.track("Android event name"); - } - }); - } + trackButton.setOnClickListener(v -> { + Timber.i("track onClick"); + MainActivity.this.doSafeClientAction(() -> ldClient.track("Android event name")); }); } private void setupIdentifyButton() { Button identify = findViewById(R.id.identify_button); - identify.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Timber.i("identify onClick"); - String userKey = ((EditText) MainActivity.this.findViewById(R.id.userKey_editText)).getText().toString(); - final LDUser updatedUser = new LDUser.Builder(userKey).build(); - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.identify(updatedUser); - } - }); - } + identify.setOnClickListener(v -> { + Timber.i("identify onClick"); + String userKey = ((EditText) MainActivity.this.findViewById(R.id.userKey_editText)).getText().toString(); + final LDUser updatedUser = new LDUser.Builder(userKey).build(); + MainActivity.this.doSafeClientAction(() -> ldClient.identify(updatedUser)); }); } private void setupOfflineSwitch() { Switch offlineSwitch = findViewById(R.id.offlineSwitch); - offlineSwitch.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { - @Override - public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { - if (isChecked) { - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.setOffline(); - } - }); - } else { - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.setOnline(); - } - }); - } - } - }); + offlineSwitch.setOnCheckedChangeListener((compoundButton, isChecked) -> + MainActivity.this.doSafeClientAction(isChecked ? () -> ldClient.setOffline() : () -> ldClient.setOnline()) + ); } private void setupEval() { @@ -238,85 +174,46 @@ private void setupEval() { spinner.setAdapter(adapter); Button evalButton = findViewById(R.id.eval_button); - evalButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - Timber.i("eval onClick"); - final String flagKey = ((EditText) MainActivity.this.findViewById(R.id.feature_flag_key)).getText().toString(); - - String type = spinner.getSelectedItem().toString(); - final String result; - String logResult; - switch (type) { - case "String": - result = MainActivity.this.doSafeClientGet(new LDClientGetFunction() { - @Override - public String get() { - return ldClient.stringVariation(flagKey, "default"); - } - }); - logResult = result == null ? "no result" : result; - Timber.i(logResult); - ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); - MainActivity.this.doSafeClientAction(new LDClientFunction() { - @Override - public void call() { - ldClient.registerFeatureFlagListener(flagKey, new FeatureFlagChangeListener() { - @Override - public void onFeatureFlagChange(String flagKey1) { - ((TextView) MainActivity.this.findViewById(R.id.result_textView)) - .setText(ldClient.stringVariation(flagKey1, "default")); - } - }); - } - }); - break; - case "Boolean": - result = MainActivity.this.doSafeClientGet(new LDClientGetFunction() { - @Override - public String get() { - return ldClient.boolVariation(flagKey, false).toString(); - } - }); - logResult = result == null ? "no result" : result; - Timber.i(logResult); - ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); - break; - case "Integer": - result = MainActivity.this.doSafeClientGet(new LDClientGetFunction() { - @Override - public String get() { - return ldClient.intVariation(flagKey, 0).toString(); - } - }); - logResult = result == null ? "no result" : result; - Timber.i(logResult); - ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); - break; - case "Float": - result = MainActivity.this.doSafeClientGet(new LDClientGetFunction() { - @Override - public String get() { - return ldClient.floatVariation(flagKey, 0F).toString(); - } - }); - logResult = result == null ? "no result" : result; - Timber.i(logResult); - ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); - break; - case "Json": - result = MainActivity.this.doSafeClientGet(new LDClientGetFunction() { - @Override - public String get() { - return ldClient.jsonVariation(flagKey, JsonNull.INSTANCE).toString(); - } - }); - logResult = result == null ? "no result" : result; - Timber.i(logResult); - ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); - break; - } + evalButton.setOnClickListener(v -> { + Timber.i("eval onClick"); + final String flagKey = ((EditText) MainActivity.this.findViewById(R.id.feature_flag_key)).getText().toString(); + + String type = spinner.getSelectedItem().toString(); + final String result; + String logResult; + switch (type) { + case "String": + result = MainActivity.this.doSafeClientGet(() -> ldClient.stringVariation(flagKey, "default")); + logResult = result == null ? "no result" : result; + Timber.i(logResult); + ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); + MainActivity.this.doSafeClientAction(() -> { + ldClient.registerFeatureFlagListener(flagKey, flagKey1 -> { + ((TextView) MainActivity.this.findViewById(R.id.result_textView)) + .setText(ldClient.stringVariation(flagKey1, "default")); + }); + }); + return; + case "Boolean": + result = MainActivity.this.doSafeClientGet(() -> String.valueOf(ldClient.boolVariation(flagKey, false))); + break; + case "Integer": + result = MainActivity.this.doSafeClientGet(() -> String.valueOf(ldClient.intVariation(flagKey, 0))); + break; + case "Float": + result = MainActivity.this.doSafeClientGet(() -> String.valueOf(ldClient.doubleVariation(flagKey, 0.0))); + break; + case "Value": + result = MainActivity.this.doSafeClientGet(() -> String.valueOf(ldClient.jsonValueVariation(flagKey, null))); + break; + default: + result = null; + break; } + + logResult = result == null ? "no result" : result; + Timber.i(logResult); + ((TextView) MainActivity.this.findViewById(R.id.result_textView)).setText(result); }); } diff --git a/example/src/main/res/values/strings.xml b/example/src/main/res/values/strings.xml index 6994a18a..c9f536fe 100644 --- a/example/src/main/res/values/strings.xml +++ b/example/src/main/res/values/strings.xml @@ -5,6 +5,6 @@ String Integer Float - Json + Value diff --git a/example/src/main/res/values/styles.xml b/example/src/main/res/values/styles.xml index 5885930d..7d5f31be 100644 --- a/example/src/main/res/values/styles.xml +++ b/example/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ -