From 27e07039c99d69e87f5921f8a55d1ef9142c77e5 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Tue, 23 Aug 2022 13:18:21 -0700 Subject: [PATCH] prepare 3.1.8 release (#184) * Removed unnecessary ListenableFuture in LDClient init, changed identify method for multi-environment, not sure about SettableFuture null vs null for return type of Future, both type check and pass tests * Added setOnlineStatus multi-environment changes * Moved primaryEnvironmentName to LDConfig, simplifying by removing primaryKey separation everywhere but the builder. Add getRequestBuilderFor a specific environment. Add static method to LDClient to get all environment names so that environments can be iterated over. Add accessor to retrieve LDClient specific UserManager. Iterate over all environments in PollingUpdater. Add environment argument to UserManager constructor, removing singleton and creating replacing init with newInstance static method. * Add back in constructor without environment to UserManager * Specialize HttpFeatureFlagFetcher to the environment, looks like UserManager may not be able to do the same so removed the environment from the constructor. * Added SharedPreferences migration strategy * All tests pass, fixed migration strategy to conform to spec, fixed primaryInstance null when offline in init, fixed primaryEnvironmentName being added to secondaryMobileKeys * Update StreamUpdateProcessor construct to take an environment for the authorization header key. * Fix issue with LDConfig mobileKeys hashmap creation. * Combine futures so LDClient init future waits on all online instances of LDClient. * Propagate IOException on closing instances to caller. * Merge futures for identify call. * Some changes from code review. * Removed static from instanceId and now old SharedPreferences will only cleared once all environments have a copy in LDClient * Fixed instanceId * Updates from PR review. * Added version and flagVersion, if available * refactor(LDClient, LDConfig): changes for PR * refactor(LDClient): changed isInternetConnected behavior for PR * refactor(LDClient): removed async getForMobileKeys and wait seconds version, replaced with method that returns requested instance * Bugfix/timber cleanup (#92) Relates to launchdarkly/android-client#60 Cleaned up timber logging messages to use string formatting rather than concatenation. Log messages should remain the same as before. Also replaced Log with Timber in the example app. * Fix crash when example app is backgrounded twice. * Add security provider update mechanism using Google Play Services to attempt a provider update when TLSv1.2 is not available. * Shared Preferences Fix for Multi Environment (#94) * fix(SharedPreferences): added more SharedPreferences first time migration and differentiated SharedPreferences by mobile key * fix(UserLocalSharePreference.java): added missing mobileKey additions to getSharedPreferences, cleaned up debugging code * Fix edge cases in how multi-environment handles connection changes. * fix(UserManagerTest.java): incorrect number of arguments to UserManager instantiation in unit test * Remove line of testing code accidentally left in and refactor shared preferences migration to make future migrations easier. * Final fixes to store migration. Should be fairly future proof. * Fix issue with primitive variation calls always returning null if fallback is null. * Remove CircleCI V1 config file. (#97) * Remove getting/comparing versions as floats (#99) To prevent floating point errors in flag version comparisons. * Include values in unknown summary events and compare values for (#100) equality when creating new summary counters. * simplify flag property deserialization * rm debugging * misc cleanup * rm debugging * add eval reason data model classes * misc fixes * serialize reason * add ability to receive evaluation reasons from LD * Changed shared preferences store system to user a single FlagStore system that holds all the information on a flag to prevent issues arising from unsynchronized separate stores for flag meta-data and values. * add methods to get value with explanation; refactor existing variation methods * Abstract FlagStoreManager from FlagStore, new FlagStoreFactory class so manager can construct FlagStores of unknown type. Reformatted interfaces. Removed unused imports. * Handle null case in allFlags, actually commit changes to UserManager. * Hopefully fix edge cases in summary event reporting to pass testing. * Hopefully fix edge cases in summary event reporting to pass testing. * Simplify getFeaturesJsonObject as no longer using -1 as placeholder for null for variations. * Make Flag non-mutable. Move GsonCache to gson package, move custom serializer/deserializers to classes in gson package and create one for PUT responses. Removed BaseUserSharedPreferences. * Send summary event even if stored flag doesn't exist. * Move sendSummaryEvent update code to UserSummaryEventSharedPreferences to synchronize to prevent data race on sending, updating, and clearing event store. Move SummaryEventSharedPreferences and UserSummaryEventSharedPreferences out of response package. * Update SharedPrefsFlagStore to hold StoreUpdatedListener in weak reference. Fix various warnings. * Migration code for upcoming flagstore. * Remove couple of debug messages. * Handle todos. * Revert to old String behavior for allFlags, initialize WeakReference in SharedPrefsFlagStore. * Better implementation of EvaluationReason serialization type adapter. * Revert "Better implementation of EvaluationReason serialization type adapter." Wrong branch... This reverts commit 69c1c9b2b8d9a3b72fcd856f2b6da0e8c896802c. * Gw/ch29266/flagstore (#105) * Changed shared preferences store system to user a single FlagStore system that holds all the information on a flag to prevent issues arising from unsynchronized separate stores for flag meta-data and values. * Abstract FlagStoreManager from FlagStore, new FlagStoreFactory class so manager can construct FlagStores of unknown type. Reformatted interfaces. Removed unused imports. * Handle null case in allFlags, actually commit changes to UserManager. * Hopefully fix edge cases in summary event reporting to pass testing. * Hopefully fix edge cases in summary event reporting to pass testing. * Simplify getFeaturesJsonObject as no longer using -1 as placeholder for null for variations. * Make Flag non-mutable. Move GsonCache to gson package, move custom serializer/deserializers to classes in gson package and create one for PUT responses. Removed BaseUserSharedPreferences. * Send summary event even if stored flag doesn't exist. * Move sendSummaryEvent update code to UserSummaryEventSharedPreferences to synchronize to prevent data race on sending, updating, and clearing event store. Move SummaryEventSharedPreferences and UserSummaryEventSharedPreferences out of response package. * Update SharedPrefsFlagStore to hold StoreUpdatedListener in weak reference. Fix various warnings. * Migration code for upcoming flagstore. * Remove couple of debug messages. * Handle todos. * Revert to old String behavior for allFlags, initialize WeakReference in SharedPrefsFlagStore. * Better implementation of EvaluationReason serialization type adapter. * Remove isUnknown argument from SummaryEventSharedPreferences methods. Use Runnable instead of Callable in UserManager to avoid useless return nulls. Rename FlagStoreFactoryInterface to FlagStoreFactory. * Statically initialize Gson instance in GsonCache. * Make Gson instance in GsonCache final on principle. * Return json flags as JsonElement in allFlags map. (#106) * Bump ok-http version to 3.9.1 (#107) * fix annotations so eval reasons are serialized in events * fix/expand doc comments for public methods * typo * typo * add version string getter method * Check for null key before file comparison check. (#110) * [ch33658] Add unsafeReset() for LDClient testing re-initialization (#111) Add `unsafeReset()` method to close and clear instances for re-initializing client between tests. Update LDClientTest to call `unsafeReset()` before tests. * [ch33846] Rename tests to not start with capitals and general refactoring (#112) * Rename tests to not start with capitals * Reindent MultiEnvironmentLDClientTest to be consistent * Optimize imports * Move TLS patch into TLSUtils * Make setModernTlsVersionsOnSocket private and remove redundant null check * Remove code duplication in LDClient track overloaded methods. * Remove validateParameter in LDClient that was using a NullPointerException as a null test. * Simplify Debounce to use listener instead of callback. * Add documentation for flagstore implementation (#113) * [ch35150] Unit tests and bug fixes (#114) - Use android test orchestrator to run tests isolated from each other. This prevents the issues testing singletons. Also enabled option to clear package data between runs allowing more extensive flagstore testing. - Remove unsafe reset as it was added only for allowing testing the LDClient singleton. - Tests for new FlagStore code. - Convenience test FlagBuilder - Fix Migration to not turn all flags into Strings - Fix issue with clearAndApplyFlagUpdates not generating correct events for listeners. * Add compatibility behavior to stringVariation and allFlags methods. (#115) If a Json flag is requested with stringVariation it will serialize it to a String. Json flags will also be serialized to Strings for the map returned by allFlags() * Update LDUser not to store all fields as Json. (#116) Add testing rule to setup and teardown Timber trees for debug logging. Add additional LDUser tests. Fixed a bit of flakiness in deletesOlderThanLastFiveStoredUsers test that showed up all of a sudden. * Add metricValue field to CustomEvent, add overloaded track method for (#118) creating custom events with metricValues. * [ch37794] Run connected emulator tests in CircleCI (#120) * [ch34533] connection status, removing guava, network restructuring. (#117) * Add ConnectionInformation class. * Remove all internal uses of Guava. * Update StreamUpdateProcessor to only debounce ping events. * Add a connection state monitor to the example app. * rename repo and package name and apply markdown templates (#121) * Fix issue that stream could be started before stopping when calling identify. (#122) * Revert "Fix issue that stream could be started before stopping when calling identify. (#122)" This reverts commit fdede38cf58af2802a116599580ea64a07d7dc4a. * Revert "rename repo and package name and apply markdown templates (#121)" This reverts commit 221527594dd632322aaf7edafa0b5edff719132e. * Revert "Revert "Fix issue that stream could be started before stopping when calling identify. (#122)"" This reverts commit 08498127157e038ec3e9d29203d29ea09c326679. * Revert "Revert "rename repo and package name and apply markdown templates (#121)"" This reverts commit bbbeb8103b58764a57fddb7bc72b93608702dbe7. * Fix thread leak on identify call from restarting EventProcessor without shutting it down first. (#123) * Add top level try/catch to migration methods. Check flag version SharedPreferences object for String type before cast. (#124) * Update Throttler to call runnable on background thread. (#125) * Fix ConcurrentModificationException of instance map (#126) Move iteration over client instances for ConnectivityReceiver and PollingUpdater to within LDClient to allow synchronizing on initialization. * adding a circleci badge to the readme (#127) * Fix bug where `stop` in StreamUpdateProcessor could not call it's listener when the stream is already closed. This caused a race condition in repeated stream restarts that could put the SDK in a bad state. * Change LDAwaitFuture to not treat zero timeout as unlimited timeout Treating a timeout of zero as unlimited caused a change in behavior when initializing the SDK. This update restores the behavior init had when zero was passed as the timeout argument from pre-2.8.0. Also improves handling of spurious wakeups, and includes test cases for LDAwaitFuture. * Revert "Merge remote-tracking branch 'remotes/origin/experiment' into next-release" This reverts commit 3ac167fb01c5d6545cf91af7817818e313108f80, reversing changes made to d26e00666a89997c4b548bc54fd82882e518b4bc. * CircleCI fixes (#131) * Better ci fix (#132) * Speedup tests by building on macOS (#137) * Background identify fixes (#133) Add new testing controllers for network and foreground states. For network control, mobile data must be disabled on recent Android versions, updated circleci config to do this. Add new connectivity manager tests. Made EventProcessor and UserManager minimal interfaces for mocking, with actual implementations moved to DefaultEventProcessor and DefaultUserManager. Fixed issue with blocking in background modes. * Experimentation 1.5 updates (#134) * add entire compile-time classpath to javadoc classpath * javadoc fixes:

is not a thing * do fail on javadoc errors * add javadoc step, misc CI cleanup * misc javadoc fixes * remove unintentional(?) immediate event flush; clean up event tests * remove unreliable test assumption about elapsed time * [ch57098] Deprecate LDCountryCode (#141) Deprecate LDCountryCode class and LDUser setters that take LDCountryCode as an argument. * Catch `SecurityException` when setting alarm in case there are already (#143) the maximum allowed number of alarms on Samsung devices. * Revert "[ch57098] Deprecate LDCountryCode (#141)" so we can do a patch release first. This reverts commit c0e71ae1214f6227f2643c467c26bdd1c07ec531. * Revert "Revert "[ch57098] Deprecate LDCountryCode (#141)" so we can do a patch release" This reverts commit 23b930ff0ff503a50af8c0ee4dcb294f688deb82. * Deprecate public classes (#145) * Deprecate some unnecessarily public classes, duplicate classes as non-public to avoid using the deprecated classes. * [ch61092] Add event payload ID. (#147) * Add event retry. (#149) * Fix javadoc comment for release. * Fix broken merge. * [ch65133] Deprecate classes (#150) * Deprecate UserSummaryEventSharedPreferences, SummaryEventSharedPreferences, FeatureFlagFetcher, Util, Debounce. * Improve Javadoc and reduce interface clutter. (#152) * Save Javadoc artifact and include logcat in circle output with tee. (#153) * Save Javadoc artifact on circleci. * Add step to kill emulator after tests, and tee output of logcat for visibility during run. * [ch62120] Background during identify callback (#154) * Adding more connectivity manager tests. * Updated internal `Foreground` class to call listeners on a background thread. * Add some comments explaining the behavior of test controllers. * Adding fixes for cases where the completion callback may not be called. * [ch65914] Diagnostic events (#156) * [ch65352] Expose LDValue rather than Gson types (#158) * Remove SET_ALARM permission. The comment that this was required for background updating is incorrect, this permission is only for sending broadcasts to an alarm clock application, something we do not do, and should never do. (#159) * Fix minimum diagnostic recording interval comment. (#160) * Data since date was not getting reset after each periodic diagnostic event. (#161) * [ch75315] Add maxCachedUsers configuration option (#162) Adds maxCachedUsers configuration option for configuring the limit on how many users have their flags cached locally. * Configure okhttp cache for polling requests to be stored in a subdirectory of the application cache directory. (#164) * Fixes ch76614 and add test of null fallback unknown flag event generation. Also some finishing touches to LDValue changes, including LDClientInterface updates, more tests, and improvements to null behavior handling. (#163) * Removing ldvalue changes before release (#165) * Revert "[ch65352] Expose LDValue rather than Gson types (#158)" This reverts commit 1e29a827 * Fixes after revert. * [ch69437] Support for setting additional headers to be included in requests. (#166) * [ch89933] Improve resiliency of store for summary events. (#167) See launchdarkly/android-client-sdk#105 for the original issue. * [ch94053] Improve throttler behavior. (#169) * Add doubleVariation, doubleVariationDetail. (#171) Deprecates floatVariation, floatVariationDetail. * Provide pollUri configuration and deprecate baseUri. (#172) * Fix throttler behavior to ensure attempt count resets are not cancelled (#178) * [ch98336] Broaden catch statement on scheduling polling alarm (#181) This is to handle more than just the SecurityException that Samsung throws, as we've gotten an issue report that some devices throw a IllegalStateException instead. * Removed the guides link * Include flag key in warning message when converting a json flag to a string (#185) * (2.x) Prevent NullPointerException when diagnostic processor shut down before starting. (#210) * Release 2.14.2 (#130) ## [2.14.2] - 2021-06-02 ### Fixed - Added check to prevent `NullPointerException` in `DiagnosticEventProcessor.stopScheduler` when `LDClient.close` is called before the application is foregrounded when the SDK was initialized in the background. ([#127](https://github.com/launchdarkly/android-client-sdk/issues/127)) - Log message warning that JSON flag was requested as a String has been updated to include the key of the flag requested to assist in discovering which flag is being requested with an unexpected type. ([#116](https://github.com/launchdarkly/android-client-sdk/issues/116)) * Bump version and update changelog for release. * Explicitly specify android:exported attribute on manifest receivers. (#211) * Update java common (#212) * Flag PendingIntent on new enough platforms as the flag is required on Android S+ (#213) * Add try for getting network capabilities (#214) * ch103537 bump java-sdk-common to 1.2 to support inExperiment on eval reason (#215) * Remove `allowBackup` manifest attribute that can conflict with the application's (#217) * Update the version to 2.8.9 * Add explicit proguard directives for keeping BroadcastReceivers. (#219) * Using the version that still support Java 8 but pin the grgit core behind the scene * Remove Android Appcompat dependency (#222) * Bump dependencies and reorganize Gradle file somewhat. (#223) * Add the null check to prevent multiple allocation of the DiagnosticEventProcessor * Fix sonatype release plugin (#226) * Add .ldrelease configuration (#227) * Add contract test service (#228) * Fix test service failing on later API versions (#229) * Add usesCleartextTraffic=true to contract-tests AndroidManifest This allows the contract tests to work on API level 28 and above * Fix start-emulator.sh to pick the newest image instead of the oldest * Refactor CI config into separate jobs with a matrix (#230) * Don't auto-retry emulator tests (#231) * Add contract tests for API level 21 (#232) * Remove unnecessary locking in LDClient (#233) * Remove `synchronized` keywords from every `LDClient` method * Treat `instances` as immutable, and swap out the whole map after constructing all the clients * Use a lock to ensure we don't try to init twice * Update `ConnectivityManager` so it now manages `DiagnosticEventManager` * Run contract tests on Android 31, 33 (#234) * Unsuppress streaming/requests and polling/requests (#236) * don't create a new executor just to trigger a flush * remove short publishing timeout, use defaults of 60 retries & 10 seconds * Serialize null values of `anonymous` as null (#237) * fix URL path concatenation to avoid double slashes * fix NPE in edge case where variation is null but value isn't * use SecureRandom instead of Random, just to make scanners happier * rm unused * fix deletion versioning logic, implement tombstones (#244) * disable contract tests for API 31/33 * use okhttp-eventsource 1.11.3 * ensure timed-out clients get closed in contract tests * clean up instances map on close (#247) * clean up instances map on close * improve atomicity of access to instances, ensure they can't be modified via closed clients * update more methods that iterate over instances * rm unnecessary LDClientControl Co-authored-by: torchhound Co-authored-by: Gavin Whelan Co-authored-by: torchhound Co-authored-by: Arun Bhalla Co-authored-by: jamesthacker Co-authored-by: Gavin Whelan Co-authored-by: torchhound Co-authored-by: Eli Bishop Co-authored-by: Ben Woskow <48036130+bwoskow-ld@users.noreply.github.com> Co-authored-by: Ben Woskow Co-authored-by: Elliot <35050275+Apache-HB@users.noreply.github.com> Co-authored-by: Robert J. Neal Co-authored-by: Louis Chan Co-authored-by: Louis Chan <91093020+louis-launchdarkly@users.noreply.github.com> Co-authored-by: Alex Engelberg Co-authored-by: LaunchDarklyReleaseBot --- .circleci/config.yml | 31 ++++--- build.gradle | 7 +- .../sdk/android/LDClientControl.java | 21 ----- .../launchdarkly/sdktest/SdkClientEntity.java | 12 ++- launchdarkly-android-client-sdk/build.gradle | 2 +- .../sdk/android/DefaultUserManagerTest.java | 55 +++--------- .../launchdarkly/sdk/android/FlagBuilder.java | 4 +- .../sdk/android/FlagStoreTest.java | 6 +- .../sdk/android/LDClientTest.java | 4 +- .../sdk/android/SharedPrefsFlagStoreTest.java | 44 ++++++---- .../sdk/android/DefaultEventProcessor.java | 4 +- .../sdk/android/DeleteFlagResponse.java | 19 ++-- .../com/launchdarkly/sdk/android/Flag.java | 36 ++++---- .../sdk/android/HttpFeatureFlagFetcher.java | 6 +- .../launchdarkly/sdk/android/LDClient.java | 87 +++++++++++-------- .../com/launchdarkly/sdk/android/LDUtil.java | 5 +- .../sdk/android/StreamUpdateProcessor.java | 4 +- .../launchdarkly/sdk/android/Throttler.java | 6 +- .../sdk/android/DeleteFlagResponseTest.java | 32 +++---- .../launchdarkly/sdk/android/FlagBuilder.java | 4 +- .../launchdarkly/sdk/android/FlagTest.java | 38 +++----- testharness-suppressions.txt | 16 ---- 22 files changed, 206 insertions(+), 237 deletions(-) delete mode 100644 contract-tests/src/main/java/com/launchdarkly/sdk/android/LDClientControl.java diff --git a/.circleci/config.yml b/.circleci/config.yml index 3c67b493..3b3ce6bb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,6 +15,11 @@ workflows: # - 21 - TODO: EasyMock makes our tests fail - 25 # - 29 - TODO: offline tests fail +# Contract tests are temporarily disabled for API 31 and API 33 due to a degree of +# test instability that impedes development. Currently there are two common failure +# modes: SDK initialization times out, or SDK initialization completes immediately +# without acquiring any data because the emulator reports no network availability. +# These do not appear correlated to any actual SDK logic flaw. - contract-tests: matrix: parameters: @@ -22,19 +27,19 @@ workflows: - 21 - 25 - 30 - - 31 - # "default" images are faster than "google_apis" images, but are otherwise equivalent for our purposes. - # however, there are no "default" images for Android 32+, so as a workaround we have a separate matrix - # for Android 32+ - - contract-tests: - matrix: - parameters: - api-level: - - 33 - system-image-type: - - google_apis - resource-class: - - xlarge +# - 31 + # # "default" images are faster than "google_apis" images, but are otherwise equivalent for our purposes. + # # however, there are no "default" images for Android 32+, so as a workaround we have a separate matrix + # # for Android 32+ + # - contract-tests: + # matrix: + # parameters: + # api-level: + # - 33 + # system-image-type: + # - google_apis + # resource-class: + # - xlarge commands: check-emulator-available: diff --git a/build.gradle b/build.gradle index 60b702ad..36100328 100644 --- a/build.gradle +++ b/build.gradle @@ -64,9 +64,4 @@ nexusPublishing { repositories { sonatype() } - - transitionCheckOptions { - maxRetries.set(20) - delayBetween.set(Duration.ofMillis(3000)) - } -} \ No newline at end of file +} diff --git a/contract-tests/src/main/java/com/launchdarkly/sdk/android/LDClientControl.java b/contract-tests/src/main/java/com/launchdarkly/sdk/android/LDClientControl.java deleted file mode 100644 index 6dcc03d9..00000000 --- a/contract-tests/src/main/java/com/launchdarkly/sdk/android/LDClientControl.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.launchdarkly.sdk.android; - -import android.app.Application; - -/** - * A class that is only here to allow the contract test service to instantiate multiple - * LDClients. Contains one static method `resetInstances()` that resets the static global - * state in LDClient. - */ -public class LDClientControl { - - /** - * Resets the global state that prevents creating more than one LDClient. - * - * This is a workaround that allows testing the Android SDK from a long-lived - * test service. - */ - public static void resetInstances() { - LDClient.instances = null; - } -} diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java index ec091443..5fcf0d59 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -6,7 +6,6 @@ import com.launchdarkly.sdk.android.LaunchDarklyException; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; -import com.launchdarkly.sdk.android.LDClientControl; import com.launchdarkly.sdktest.Representations.AliasEventParams; import com.launchdarkly.sdktest.Representations.CommandParams; @@ -45,8 +44,6 @@ public class SdkClientEntity { public SdkClientEntity(Application application, CreateInstanceParams params) { Timber.i("Creating client for %s", params.tag); - LDClientControl.resetInstances(); - Timber.i("Reset global state to allow for another client"); LDConfig config = buildSdkConfig(params.configuration); // Each new client will plant a new Timber tree, so we uproot any existing ones // to avoid spamming stdout with duplicate log lines @@ -70,6 +67,9 @@ public SdkClientEntity(Application application, CreateInstanceParams params) { this.client = LDClient.get(); if (!client.isInitialized() && !params.configuration.initCanFail) { // If `initCanFail` is true, we can proceed with an uninitialized client + try { + client.close(); + } catch (IOException e) {} throw new RuntimeException("client initialization failed or timed out"); } } catch (LaunchDarklyException e) { @@ -171,7 +171,11 @@ private EvaluateAllFlagsResponse doEvaluateAll(EvaluateAllFlagsParams params) { } private void doIdentifyEvent(IdentifyEventParams params) { - client.identify(params.user); + try { + client.identify(params.user).get(); + } catch (ExecutionException | InterruptedException e) { + throw new RuntimeException("Error waiting for identify", e); + } } private void doCustomEvent(CustomEventParams params) { diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index 2353751e..bc652b9c 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -55,7 +55,7 @@ configurations { ext {} ext.versions = [ "androidAnnotation": "1.2.0", - "eventsource": "1.11.2", + "eventsource": "1.11.3", "gson": "2.8.9", "jacksonCore": "2.10.5", "jacksonDatabind": "2.10.5.1", diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java index 127cf04a..80bf0bd2 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DefaultUserManagerTest.java @@ -195,8 +195,11 @@ public void testDeleteFlag() throws ExecutionException { AwaitableCallback deleteAwait = new AwaitableCallback<>(); userManager.deleteCurrentUserFlag("{\"key\":\"stringFlag1\",\"version\":16}", deleteAwait); deleteAwait.await(); - assertNull(flagStore.getFlag("stringFlag1")); - assertEquals(true, flagStore.getFlag("boolFlag1").getValue().booleanValue()); + Flag updated = flagStore.getFlag("stringFlag1"); + assertNotNull(updated); + assertEquals("stringFlag1", updated.getKey()); + assertEquals(16, updated.getVersion()); + assertTrue(updated.isDeleted()); deleteAwait.reset(); userManager.deleteCurrentUserFlag("{\"key\":\"nonExistentFlag\",\"version\":16,\"value\":false}", deleteAwait); @@ -274,7 +277,8 @@ public void testDeleteWithVersion() throws ExecutionException { userManager.deleteCurrentUserFlag("{\"key\":\"stringFlag1\",\"version\":127}", awaitableCallback); awaitableCallback.await(); awaitableCallback.reset(); - assertNull(flagStore.getFlag("stringFlag1")); + assertNotNull(flagStore.getFlag("stringFlag1")); + assertTrue(flagStore.getFlag("stringFlag1").isDeleted()); userManager.deleteCurrentUserFlag("{\"key\":\"nonExistent\",\"version\":1}", awaitableCallback); awaitableCallback.await(); @@ -323,32 +327,13 @@ public void testPatchSucceedsForMissingVersionInPatch() throws ExecutionExceptio FlagStore flagStore = userManager.getCurrentUserFlagStore(); AwaitableCallback awaitableCallback = new AwaitableCallback<>(); - // version does not exist in shared preferences and patch. - // --------------------------- - //// case 1: value does not exist in shared preferences. - userManager.patchCurrentUserFlags("{\"key\":\"flag1\",\"value\":\"value-from-patch\"}", awaitableCallback); - awaitableCallback.await(); - awaitableCallback.reset(); - Flag flag1 = flagStore.getFlag("flag1"); - assertEquals("value-from-patch", flag1.getValue().stringValue()); - assertNull(flag1.getVersion()); - - //// case 2: value exists in shared preferences without version. - userManager.putCurrentUserFlags("{\"flag1\": {\"value\": \"value1\"}}", null); - userManager.patchCurrentUserFlags("{\"key\":\"flag1\",\"value\":\"value-from-patch\"}", awaitableCallback); - awaitableCallback.await(); - awaitableCallback.reset(); - flag1 = flagStore.getFlag("flag1"); - assertEquals("value-from-patch", flag1.getValue().stringValue()); - assertNull(flag1.getVersion()); - // version does not exist in shared preferences but exists in patch. // --------------------------- //// case 1: value does not exist in shared preferences. userManager.patchCurrentUserFlags("{\"key\":\"flag1\",\"version\":558,\"flagVersion\":3,\"value\":\"value-from-patch\",\"variation\":1,\"trackEvents\":false}", awaitableCallback); awaitableCallback.await(); awaitableCallback.reset(); - flag1 = flagStore.getFlag("flag1"); + Flag flag1 = flagStore.getFlag("flag1"); assertEquals("value-from-patch", flag1.getValue().stringValue()); assertEquals(558, (int) flag1.getVersion()); assertEquals(3, (int) flag1.getFlagVersion()); @@ -361,21 +346,9 @@ public void testPatchSucceedsForMissingVersionInPatch() throws ExecutionExceptio awaitableCallback.reset(); flag1 = flagStore.getFlag("flag1"); assertEquals("value-from-patch", flag1.getValue().stringValue()); - assertEquals(558, (int) flag1.getVersion()); - assertEquals(3, (int) flag1.getFlagVersion()); - assertEquals(3, (int) flag1.getVersionForEvents()); - - // version exists in shared preferences but does not exist in patch. - // --------------------------- - userManager.putCurrentUserFlags("{\"flag1\": {\"version\": 558, \"flagVersion\": 110,\"value\": \"value1\", \"variation\": 1, \"trackEvents\": false}}", null); - userManager.patchCurrentUserFlags("{\"key\":\"flag1\",\"value\":\"value-from-patch\"}", awaitableCallback); - awaitableCallback.await(); - awaitableCallback.reset(); - flag1 = flagStore.getFlag("flag1"); - assertEquals("value-from-patch", flag1.getValue().stringValue()); - assertNull(flag1.getVersion()); - assertNull(flag1.getFlagVersion()); - assertNull(flag1.getVersionForEvents()); + assertEquals(558, flag1.getVersion()); + assertEquals(Integer.valueOf(3), flag1.getFlagVersion()); + assertEquals(3, flag1.getVersionForEvents()); // version exists in shared preferences and patch. // --------------------------- @@ -384,9 +357,9 @@ public void testPatchSucceedsForMissingVersionInPatch() throws ExecutionExceptio awaitableCallback.await(); flag1 = flagStore.getFlag("flag1"); assertEquals("value-from-patch", flag1.getValue().stringValue()); - assertEquals(559, (int) flag1.getVersion()); - assertEquals(3, (int) flag1.getFlagVersion()); - assertEquals(3, (int) flag1.getVersionForEvents()); + assertEquals(559, flag1.getVersion()); + assertEquals(Integer.valueOf(3), flag1.getFlagVersion()); + assertEquals(3, flag1.getVersionForEvents()); } @Test diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagBuilder.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagBuilder.java index 6c7b5475..39c335ba 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagBuilder.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagBuilder.java @@ -10,7 +10,7 @@ public class FlagBuilder { @NonNull private String key; private LDValue value = null; - private Integer version = null; + private int version; private Integer flagVersion = null; private Integer variation = null; private Boolean trackEvents = null; @@ -27,7 +27,7 @@ public FlagBuilder value(LDValue value) { return this; } - public FlagBuilder version(Integer version) { + public FlagBuilder version(int version) { this.version = version; return this; } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagStoreTest.java index a2f1bf58..70c7cdd5 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/FlagStoreTest.java @@ -51,7 +51,7 @@ public boolean matches(Object argument) { Objects.equals(flag.getVersion(), received.getVersion()) && Objects.equals(flag.getFlagVersion(), received.getFlagVersion()) && Objects.equals(flag.getVariation(), received.getVariation()) && - Objects.equals(flag.getTrackEvents(), received.getTrackEvents()) && + Objects.equals(flag.isTrackEvents(), received.isTrackEvents()) && Objects.equals(flag.isTrackReason(), received.isTrackReason()) && Objects.equals(flag.getDebugEventsUntilDate(), received.getDebugEventsUntilDate()) && @@ -79,7 +79,7 @@ private void assertExpectedFlag(Flag expected, Flag received) { assertEquals(expected.getVersion(), received.getVersion()); assertEquals(expected.getFlagVersion(), received.getFlagVersion()); assertEquals(expected.getVariation(), received.getVariation()); - assertEquals(expected.getTrackEvents(), received.getTrackEvents()); + assertEquals(expected.isTrackEvents(), received.isTrackEvents()); assertEquals(expected.isTrackReason(), received.isTrackReason()); assertEquals(expected.getDebugEventsUntilDate(), received.getDebugEventsUntilDate()); assertEquals(expected.getReason(), received.getReason()); @@ -204,7 +204,7 @@ public void mockFlagDeleteBehavior() { @Test public void testUnregisterStoreUpdate() { - final Flag initialFlag = new FlagBuilder("flag").build(); + final Flag initialFlag = new FlagBuilder("flag").version(10).build(); final FlagUpdate mockCreate = strictMock(FlagUpdate.class); expect(mockCreate.flagToUpdate()).andReturn("flag"); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java index 7c235dfe..7457f2f5 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDClientTest.java @@ -602,7 +602,7 @@ public void variationFlagTrackReasonGeneratesEventWithReason() throws IOExceptio TestUtil.markMigrationComplete(application); EvaluationReason testReason = EvaluationReason.off(); FlagStore flagStore = new SharedPrefsFlagStoreFactory(application).createFlagStore(mobileKey + DefaultUserManager.sharedPrefs(ldUser)); - flagStore.applyFlagUpdate(new FlagBuilder("track-reason-flag").trackEvents(true).trackReason(true).reason(testReason).build()); + flagStore.applyFlagUpdate(new FlagBuilder("track-reason-flag").version(10).trackEvents(true).trackReason(true).reason(testReason).build()); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { client.boolVariation("track-reason-flag", false); @@ -615,7 +615,7 @@ public void variationFlagTrackReasonGeneratesEventWithReason() throws IOExceptio assertEquals("track-reason-flag", event.key); assertEquals("userKey", event.userKey); assertNull(event.variation); - assertNull(event.version); + assertEquals(Integer.valueOf(10), event.version); assertFalse(event.value.booleanValue()); assertFalse(event.defaultVal.booleanValue()); assertEquals(testReason, event.reason); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java index 285b0e7b..9fcab3cc 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/SharedPrefsFlagStoreTest.java @@ -15,6 +15,9 @@ import java.util.Collections; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; @RunWith(AndroidJUnit4.class) public class SharedPrefsFlagStoreTest extends FlagStoreTest { @@ -34,51 +37,60 @@ public FlagStore createFlagStore(String identifier) { } @Test - public void deletesVersions() { + public void deletesVersionAndStoresDeletedItemPlaceholder() { final Flag key1 = new FlagBuilder("key1").version(12).build(); final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); flagStore.applyFlagUpdates(Collections.singletonList(key1)); - flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), null)); + flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 13)); - Assert.assertNull(flagStore.getFlag(key1.getKey())); + Flag updated = flagStore.getFlag(key1.getKey()); + assertNotNull(updated); + assertEquals(key1.getKey(), updated.getKey()); + assertEquals(13, updated.getVersion()); + assertTrue(updated.isDeleted()); } @Test - public void updatesVersions() { + public void doesNotDeleteIfDeletionVersionIsLessThanOrEqualToExistingVersion() { final Flag key1 = new FlagBuilder("key1").version(12).build(); - final Flag updatedKey1 = new FlagBuilder(key1.getKey()).version(15).build(); final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); flagStore.applyFlagUpdates(Collections.singletonList(key1)); - - flagStore.applyFlagUpdate(updatedKey1); - - assertEquals(flagStore.getFlag(key1.getKey()).getVersion(), 15, 0); + flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 11)); + flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), 12)); + + Flag updated = flagStore.getFlag(key1.getKey()); + assertEquals(key1.getKey(), updated.getKey()); + assertEquals(key1.getVersion(), updated.getVersion()); + assertEquals(key1.getValue(), updated.getValue()); + assertFalse(updated.isDeleted()); } @Test - public void deletesFlagVersions() { - final Flag key1 = new FlagBuilder("key1").flagVersion(12).build(); + public void updatesVersions() { + final Flag key1 = new FlagBuilder("key1").version(12).build(); + final Flag updatedKey1 = new FlagBuilder(key1.getKey()).version(15).build(); final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); flagStore.applyFlagUpdates(Collections.singletonList(key1)); - flagStore.applyFlagUpdate(new DeleteFlagResponse(key1.getKey(), null)); - Assert.assertNull(flagStore.getFlag(key1.getKey())); + flagStore.applyFlagUpdate(updatedKey1); + + assertEquals(15, flagStore.getFlag(key1.getKey()).getVersion()); } @Test public void updatesFlagVersions() { - final Flag key1 = new FlagBuilder("key1").flagVersion(12).build(); - final Flag updatedKey1 = new FlagBuilder(key1.getKey()).flagVersion(15).build(); + final Flag key1 = new FlagBuilder("key1").version(100).flagVersion(12).build(); + final Flag updatedKey1 = new FlagBuilder(key1.getKey()).version(101).flagVersion(15).build(); final SharedPrefsFlagStore flagStore = new SharedPrefsFlagStore(testApplication, "abc"); flagStore.applyFlagUpdates(Collections.singletonList(key1)); flagStore.applyFlagUpdate(updatedKey1); - assertEquals(flagStore.getFlag(key1.getKey()).getFlagVersion(), 15, 0); + assertEquals(Integer.valueOf(15), flagStore.getFlag(key1.getKey()).getFlagVersion()); } @Test diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java index e557376a..d4ca4160 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DefaultEventProcessor.java @@ -98,7 +98,9 @@ public void close() { } public void flush() { - Executors.newSingleThreadExecutor().execute(consumer); + if (scheduler != null) { + scheduler.schedule(consumer, 0, TimeUnit.MILLISECONDS); + } } @VisibleForTesting diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DeleteFlagResponse.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DeleteFlagResponse.java index b5bcd102..bae14b19 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DeleteFlagResponse.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DeleteFlagResponse.java @@ -3,24 +3,25 @@ class DeleteFlagResponse implements FlagUpdate { private final String key; - private final Integer version; + private final int version; - DeleteFlagResponse(String key, Integer version) { + DeleteFlagResponse(String key, int version) { this.key = key; this.version = version; } /** - * Returns null to signal deletion of the flag if this update is valid on the supplied flag, - * otherwise returns the existing flag. + * Returns an updated version of the flag that is in a deleted state, if the update is valid + * (has a higher version than any existing version in the store), otherwise returns the + * existing flag. * * @param before An existing Flag associated with flagKey from flagToUpdate() - * @return null, or the before flag. + * @return the new Flag state */ @Override public Flag updateFlag(Flag before) { - if (before == null || version == null || before.isVersionMissing() || version > before.getVersion()) { - return null; + if (before == null || this.version > before.getVersion()) { + return Flag.deletedItemPlaceholder(key, version); } return before; } @@ -29,4 +30,8 @@ public Flag updateFlag(Flag before) { public String flagToUpdate() { return key; } + + public int getVersion() { + return version; + } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Flag.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Flag.java index a3d2ddfc..6baac192 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Flag.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Flag.java @@ -10,15 +10,16 @@ class Flag implements FlagUpdate { @NonNull private final String key; private final LDValue value; - private final Integer version; + private final int version; private final Integer flagVersion; private final Integer variation; private final Boolean trackEvents; private final Boolean trackReason; private final Long debugEventsUntilDate; private final EvaluationReason reason; + private final Boolean deleted; - Flag(@NonNull String key, LDValue value, Integer version, Integer flagVersion, Integer variation, Boolean trackEvents, Boolean trackReason, Long debugEventsUntilDate, EvaluationReason reason) { + private Flag(@NonNull String key, LDValue value, int version, Integer flagVersion, Integer variation, Boolean trackEvents, Boolean trackReason, Long debugEventsUntilDate, EvaluationReason reason, boolean deleted) { this.key = key; this.value = value; this.version = version; @@ -28,9 +29,17 @@ class Flag implements FlagUpdate { this.trackReason = trackReason; this.debugEventsUntilDate = debugEventsUntilDate; this.reason = reason; + this.deleted = deleted ? Boolean.valueOf(true) : null; + } + + Flag(@NonNull String key, LDValue value, int version, Integer flagVersion, Integer variation, Boolean trackEvents, Boolean trackReason, Long debugEventsUntilDate, EvaluationReason reason) { + this(key, value, version, flagVersion, variation, trackEvents, trackReason, debugEventsUntilDate, reason, false); + } + + static Flag deletedItemPlaceholder(@NonNull String key, int version) { + return new Flag(key, null, version, null, null, null, null, null, null, true); } - @NonNull String getKey() { return key; } @@ -41,7 +50,7 @@ LDValue getValue() { return LDValue.normalize(value); } - Integer getVersion() { + int getVersion() { return version; } @@ -53,11 +62,11 @@ Integer getVariation() { return variation; } - boolean getTrackEvents() { - return trackEvents == null ? false : trackEvents; + boolean isTrackEvents() { + return trackEvents != null && trackEvents.booleanValue(); } - boolean isTrackReason() { return trackReason == null ? false : trackReason; } + boolean isTrackReason() { return trackReason != null && trackReason.booleanValue(); } Long getDebugEventsUntilDate() { return debugEventsUntilDate; @@ -67,20 +76,17 @@ EvaluationReason getReason() { return reason; } - boolean isVersionMissing() { - return version == null; + int getVersionForEvents() { + return flagVersion == null ? version : flagVersion.intValue(); } - Integer getVersionForEvents() { - if (flagVersion == null) { - return version; - } - return flagVersion; + boolean isDeleted() { + return deleted != null && deleted.booleanValue(); } @Override public Flag updateFlag(Flag before) { - if (before == null || this.isVersionMissing() || before.isVersionMissing() || this.getVersion() > before.getVersion()) { + if (before == null || this.getVersion() > before.getVersion()) { return this; } return before; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java index bfa802a1..327b2e2b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/HttpFeatureFlagFetcher.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.android; import android.content.Context; +import android.net.Uri; import androidx.annotation.NonNull; @@ -107,7 +108,8 @@ public void onResponse(@NonNull Call call, @NonNull final Response response) { } private Request getDefaultRequest(LDUser user) { - String uri = config.getPollUri() + "/msdk/evalx/users/" + DefaultUserManager.base64Url(user); + String uri = Uri.withAppendedPath(config.getPollUri(), "msdk/evalx/users/").toString() + + DefaultUserManager.base64Url(user); if (config.isEvaluationReasons()) { uri += "?withReasons=true"; } @@ -118,7 +120,7 @@ private Request getDefaultRequest(LDUser user) { } private Request getReportRequest(LDUser user) { - String reportUri = config.getPollUri() + "/msdk/evalx/user"; + String reportUri = Uri.withAppendedPath(config.getPollUri(), "msdk/evalx/user").toString(); if (config.isEvaluationReasons()) { reportUri += "?withReasons=true"; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java index 07323034..5ee5c5af 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDClient.java @@ -235,14 +235,15 @@ public static LDClient get() throws LaunchDarklyException { */ @SuppressWarnings("WeakerAccess") public static LDClient getForMobileKey(String keyName) throws LaunchDarklyException { - if (instances == null) { + Map instancesNow = instances; // ensures atomicity + if (instancesNow == null) { LDConfig.log().e("LDClient.getForMobileKey() was called before init()!"); throw new LaunchDarklyException("LDClient.getForMobileKey() was called before init()!"); } - if (!(instances.containsKey(keyName))) { + if (!(instancesNow.containsKey(keyName))) { throw new LaunchDarklyException("LDClient.getForMobileKey() called with invalid keyName"); } - return instances.get(keyName); + return instancesNow.get(keyName); } @VisibleForTesting @@ -312,7 +313,23 @@ public Future identify(LDUser user) { if (user.getKey() == null) { LDConfig.log().w("identify called with null user or null user key!"); } - return LDClient.identifyInstances(customizeUser(user)); + return identifyInstances(customizeUser(user)); + } + + private @NonNull Map getInstancesIfTheyIncludeThisClient() { + // Using this method ensures that 1. we are operating on an atomic snapshot of the + // instances (in the unlikely case that they get closed & recreated right around now) and + // 2. we do *not* operate on these instances if the current client is not one of them (i.e. + // if it's already been closed). This method is guaranteed never to return null. + Map ret = instances; + if (ret != null) { + for (LDClient c: ret.values()) { + if (c == this) { + return ret; + } + } + } + return Collections.emptyMap(); } private void identifyInternal(@NonNull LDUser user, @@ -328,9 +345,10 @@ private void identifyInternal(@NonNull LDUser user, sendEvent(new IdentifyEvent(user)); } - private static Future identifyInstances(@NonNull LDUser user) { + private Future identifyInstances(@NonNull LDUser user) { final LDAwaitFuture resultFuture = new LDAwaitFuture<>(); - final AtomicInteger identifyCounter = new AtomicInteger(instances.size()); + final Map instancesNow = getInstancesIfTheyIncludeThisClient(); + final AtomicInteger identifyCounter = new AtomicInteger(instancesNow.size()); LDUtil.ResultCallback completeWhenCounterZero = new LDUtil.ResultCallback() { @Override public void onSuccess(Void result) { @@ -345,7 +363,7 @@ public void onError(Throwable e) { } }; - for (LDClient client : instances.values()) { + for (LDClient client : instancesNow.values()) { client.identifyInternal(user, completeWhenCounterZero); } @@ -357,7 +375,9 @@ public Map allFlags() { Collection allFlags = userManager.getCurrentUserFlagStore().getAllFlags(); HashMap flagValues = new HashMap<>(); for (Flag flag: allFlags) { - flagValues.put(flag.getKey(), flag.getValue()); + if (!flag.isDeleted()) { + flagValues.put(flag.getKey(), flag.getValue()); + } } return flagValues; } @@ -421,22 +441,22 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ EvaluationDetail result; LDValue value = defaultValue; - if (flag == null) { + if (flag == null || flag.isDeleted()) { LDConfig.log().i("Unknown feature flag \"%s\"; returning default value", key); result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.FLAG_NOT_FOUND)); } else { value = flag.getValue(); + int variation = flag.getVariation() == null ? EvaluationDetail.NO_VARIATION : flag.getVariation(); if (value.isNull()) { LDConfig.log().w("Feature flag \"%s\" retrieved with no value; returning default value", key); value = defaultValue; - int variation = flag.getVariation() == null ? EvaluationDetail.NO_VARIATION : flag.getVariation(); result = EvaluationDetail.fromValue(defaultValue, variation, flag.getReason()); } else if (checkType && !defaultValue.isNull() && value.getType() != defaultValue.getType()) { LDConfig.log().w("Feature flag \"%s\" with type %s retrieved as %s; returning default value", key, value.getType(), defaultValue.getType()); value = defaultValue; result = EvaluationDetail.fromValue(defaultValue, EvaluationDetail.NO_VARIATION, EvaluationReason.error(EvaluationReason.ErrorKind.WRONG_TYPE)); } else { - result = EvaluationDetail.fromValue(value, flag.getVariation(), flag.getReason()); + result = EvaluationDetail.fromValue(value, variation, flag.getReason()); } sendFlagRequestEvent(key, flag, value, defaultValue, flag.isTrackReason() | needsReason ? result.getReason() : null); } @@ -453,7 +473,7 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ */ @Override public void close() throws IOException { - LDClient.closeInstances(); + closeInstances(); } private void closeInternal() { @@ -466,27 +486,28 @@ private void closeInternal() { } } - private static void closeInstances() { - for (LDClient client : instances.values()) { + private void closeInstances() { + Iterable oldClients; + synchronized (initLock) { + oldClients = getInstancesIfTheyIncludeThisClient().values(); + instances = null; + } + for (LDClient client : oldClients) { client.closeInternal(); } } @Override public void flush() { - LDClient.flushInstances(); + for (LDClient client : getInstancesIfTheyIncludeThisClient().values()) { + client.flushInternal(); + } } private void flushInternal() { eventProcessor.flush(); } - private static void flushInstances() { - for (LDClient client : instances.values()) { - client.flushInternal(); - } - } - @VisibleForTesting void blockingFlush() { eventProcessor.blockingFlush(); @@ -504,34 +525,26 @@ public boolean isOffline() { @Override public void setOffline() { - LDClient.setInstancesOffline(); + for (LDClient client : getInstancesIfTheyIncludeThisClient().values()) { + client.setOfflineInternal(); + } } private void setOfflineInternal() { connectivityManager.setOffline(); } - private static void setInstancesOffline() { - for (LDClient client : instances.values()) { - client.setOfflineInternal(); - } - } - @Override public void setOnline() { - setOnlineStatusInstances(); + for (LDClient client : getInstancesIfTheyIncludeThisClient().values()) { + client.setOnlineStatusInternal(); + } } private void setOnlineStatusInternal() { connectivityManager.setOnline(); } - private static void setOnlineStatusInstances() { - for (LDClient client : instances.values()) { - client.setOnlineStatusInternal(); - } - } - @Override public void registerFeatureFlagListener(String flagKey, FeatureFlagChangeListener listener) { userManager.registerListener(flagKey, listener); @@ -639,9 +652,9 @@ private void onNetworkConnectivityChange(boolean connectedToInternet) { } private void sendFlagRequestEvent(String flagKey, Flag flag, LDValue value, LDValue defaultValue, EvaluationReason reason) { - Integer version = flag.getVersionForEvents(); + int version = flag.getVersionForEvents(); Integer variation = flag.getVariation(); - if (flag.getTrackEvents()) { + if (flag.isTrackEvents()) { sendEvent(new FeatureRequestEvent(flagKey, userManager.getCurrentUser(), value, defaultValue, version, variation, reason, config.inlineUsersInEvents(), false)); } else { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java index 5741715d..fbdbf9d5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDUtil.java @@ -220,7 +220,10 @@ public void write(JsonWriter out, LDUser user) throws IOException { out.beginObject(); out.name("key").value(user.getKey()); - out.name("anonymous").value(user.isAnonymous()); + + if (!user.getAttribute(UserAttribute.ANONYMOUS).isNull()) { + out.name("anonymous").value(user.isAnonymous()); + } for (UserAttribute attrib : OPTIONAL_BUILTINS) { safeWrite(out, user, attrib, privateAttrs); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java index db6f7199..250f7bb0 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StreamUpdateProcessor.java @@ -20,6 +20,8 @@ import static com.launchdarkly.sdk.android.LDConfig.GSON; import static com.launchdarkly.sdk.android.LDConfig.JSON; +import android.net.Uri; + class StreamUpdateProcessor { private static final String METHOD_REPORT = "REPORT"; @@ -151,7 +153,7 @@ private RequestBody getRequestBody(@Nullable LDUser user) { } private URI getUri(@Nullable LDUser user) { - String str = config.getStreamUri().toString() + "/meval"; + String str = Uri.withAppendedPath(config.getStreamUri(), "meval").toString(); if (!config.isUseReport() && user != null) { str += "/" + DefaultUserManager.base64Url(user); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Throttler.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Throttler.java index 880829af..ec1b2193 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Throttler.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Throttler.java @@ -2,7 +2,7 @@ import androidx.annotation.NonNull; -import java.util.Random; +import java.security.SecureRandom; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -20,7 +20,7 @@ class Throttler { private final long retryTimeMs; private final long maxRetryTimeMs; - private final Random jitter = new Random(); + private final SecureRandom jitter = new SecureRandom(); private final AtomicInteger attempts = new AtomicInteger(-1); private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); @@ -80,7 +80,7 @@ private int pow2(int k) { // Adapted from http://stackoverflow.com/questions/2546078/java-random-long-number-in-0-x-n-range // Since ThreadLocalRandom.current().nextLong(n) requires Android 5 - private long nextLong(Random rand, long bound) { + private long nextLong(SecureRandom rand, long bound) { if (bound <= 0) { throw new IllegalArgumentException("bound must be positive"); } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DeleteFlagResponseTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DeleteFlagResponseTest.java index 91c36400..ddd64d78 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DeleteFlagResponseTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/DeleteFlagResponseTest.java @@ -6,12 +6,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; public class DeleteFlagResponseTest { private static final Gson gson = GsonCache.getGson(); - private static final String jsonWithoutVersion = "{\"key\": \"flag\"}"; private static final String jsonWithNullVersion = "{\"key\": \"flag2\", \"version\": null}"; private static final String jsonWithVersion = "{\"key\": \"flag\", \"version\": 50}"; private static final String jsonWithExtraElement = "{\"key\": \"flag\", \"version\": 100, \"extra\": [1,2,3]}"; @@ -19,15 +19,13 @@ public class DeleteFlagResponseTest { @Test public void constructor() { // Cannot check flag version, as the field is not exposed. - assertNull((new DeleteFlagResponse(null, null)).flagToUpdate()); - assertEquals("test", (new DeleteFlagResponse("test", null)).flagToUpdate()); + assertNull((new DeleteFlagResponse(null, 1)).flagToUpdate()); + assertEquals("test", (new DeleteFlagResponse("test", 1)).flagToUpdate()); } @Test public void deleteFlagResponseKeyIsDeserialized() { DeleteFlagResponse result; - result = gson.fromJson(jsonWithoutVersion, DeleteFlagResponse.class); - assertEquals("flag", result.flagToUpdate()); result = gson.fromJson(jsonWithNullVersion, DeleteFlagResponse.class); assertEquals("flag2", result.flagToUpdate()); result = gson.fromJson(jsonWithVersion, DeleteFlagResponse.class); @@ -39,23 +37,25 @@ public void deleteFlagResponseKeyIsDeserialized() { @Test public void testUpdateFlag() { // Create delete flag responses from json to verify version is deserialized - DeleteFlagResponse deleteNoVersion = gson.fromJson(jsonWithoutVersion, DeleteFlagResponse.class); DeleteFlagResponse deleteLowVersion = gson.fromJson(jsonWithVersion, DeleteFlagResponse.class); DeleteFlagResponse deleteHighVersion = gson.fromJson(jsonWithExtraElement, DeleteFlagResponse.class); - Flag flagNoVersion = new FlagBuilder("flag").build(); Flag flagLowVersion = new FlagBuilder("flag").version(50).build(); Flag flagHighVersion = new FlagBuilder("flag").version(100).build(); - assertNull(deleteNoVersion.updateFlag(null)); - assertNull(deleteNoVersion.updateFlag(flagNoVersion)); - assertNull(deleteNoVersion.updateFlag(flagLowVersion)); - assertNull(deleteNoVersion.updateFlag(flagHighVersion)); - assertNull(deleteLowVersion.updateFlag(null)); - assertNull(deleteLowVersion.updateFlag(flagNoVersion)); assertEquals(flagLowVersion, deleteLowVersion.updateFlag(flagLowVersion)); assertEquals(flagHighVersion, deleteLowVersion.updateFlag(flagHighVersion)); - assertNull(deleteHighVersion.updateFlag(null)); - assertNull(deleteHighVersion.updateFlag(flagNoVersion)); - assertNull(deleteHighVersion.updateFlag(flagLowVersion)); + + assertDeletedItemPlaceholder(deleteLowVersion.updateFlag(null), + deleteLowVersion.flagToUpdate(), deleteLowVersion.getVersion()); + assertDeletedItemPlaceholder(deleteHighVersion.updateFlag(null), + deleteHighVersion.flagToUpdate(), deleteHighVersion.getVersion()); + assertDeletedItemPlaceholder(deleteHighVersion.updateFlag(flagLowVersion), + deleteHighVersion.flagToUpdate(), deleteHighVersion.getVersion()); + } + + private static void assertDeletedItemPlaceholder(Flag f, String key, int version) { + assertEquals(key, f.getKey()); + assertEquals(version, f.getVersion()); + assertTrue(f.isDeleted()); } } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagBuilder.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagBuilder.java index 6c7b5475..857b4f6a 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagBuilder.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagBuilder.java @@ -10,7 +10,7 @@ public class FlagBuilder { @NonNull private String key; private LDValue value = null; - private Integer version = null; + private int version = 0; private Integer flagVersion = null; private Integer variation = null; private Boolean trackEvents = null; @@ -27,7 +27,7 @@ public FlagBuilder value(LDValue value) { return this; } - public FlagBuilder version(Integer version) { + public FlagBuilder version(int version) { this.version = version; return this; } diff --git a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java index 881d454c..2438a0f4 100644 --- a/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java +++ b/launchdarkly-android-client-sdk/src/test/java/com/launchdarkly/sdk/android/FlagTest.java @@ -119,13 +119,6 @@ public void versionIsDeserialized() { assertEquals(99, (int) r.getVersion()); } - @Test - public void versionDefaultWhenOmitted() { - final String jsonStr = "{\"flagVersion\": 99}"; - final Flag r = gson.fromJson(jsonStr, Flag.class); - assertNull(r.getVersion()); - } - @Test public void flagVersionIsSerialized() { final Flag r = new FlagBuilder("flag").flagVersion(100).build(); @@ -180,14 +173,14 @@ public void trackEventsIsSerialized() { public void trackEventsIsDeserialized() { final String jsonStr = "{\"version\": 99, \"trackEvents\": true}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertTrue(r.getTrackEvents()); + assertTrue(r.isTrackEvents()); } @Test public void trackEventsDefaultWhenOmitted() { final String jsonStr = "{\"version\": 99}"; final Flag r = gson.fromJson(jsonStr, Flag.class); - assertFalse(r.getTrackEvents()); + assertFalse(r.isTrackEvents()); } @Test @@ -285,24 +278,21 @@ public void emptyPropertiesAreNotSerialized() { } @Test - public void testIsVersionMissing() { - final Flag noVersion = new FlagBuilder("flag").build(); - final Flag withVersion = new FlagBuilder("flag").version(10).build(); - assertTrue(noVersion.isVersionMissing()); - assertFalse(withVersion.isVersionMissing()); + public void testIsDeleted() { + final Flag normalFlag = new FlagBuilder("flag").version(10).build(); + final Flag placeholder = Flag.deletedItemPlaceholder("flag", 10); + assertFalse(normalFlag.isDeleted()); + assertTrue(placeholder.isDeleted()); + assertEquals(normalFlag.getVersion(), placeholder.getVersion()); } @Test public void testGetVersionForEvents() { - final Flag noVersions = new FlagBuilder("flag").build(); final Flag withVersion = new FlagBuilder("flag").version(10).build(); - final Flag withFlagVersion = new FlagBuilder("flag").flagVersion(5).build(); - final Flag withBothVersions = new FlagBuilder("flag").version(10).flagVersion(5).build(); + final Flag withVersionAndFlagVersion = new FlagBuilder("flag").version(10).flagVersion(5).build(); - assertNull(noVersions.getVersionForEvents()); - assertEquals(10, (int) withVersion.getVersionForEvents()); - assertEquals(5, (int) withFlagVersion.getVersionForEvents()); - assertEquals(5, (int) withBothVersions.getVersionForEvents()); + assertEquals(10, withVersion.getVersionForEvents()); + assertEquals(5, withVersionAndFlagVersion.getVersionForEvents()); } @Test @@ -313,17 +303,11 @@ public void flagToUpdateReturnsKey() { @Test public void testUpdateFlag() { - final Flag flagNoVersion = new FlagBuilder("flagNoVersion").build(); - final Flag flagNoVersion2 = new FlagBuilder("flagNoVersion2").build(); final Flag flagLowVersion = new FlagBuilder("flagLowVersion").version(50).build(); final Flag flagSameVersion = new FlagBuilder("flagSameVersion").version(50).build(); final Flag flagHighVersion = new FlagBuilder("flagHighVersion").version(100).build(); - assertEquals(flagNoVersion, flagNoVersion.updateFlag(null)); assertEquals(flagLowVersion, flagLowVersion.updateFlag(null)); - assertEquals(flagNoVersion, flagNoVersion.updateFlag(flagNoVersion2)); - assertEquals(flagLowVersion, flagLowVersion.updateFlag(flagNoVersion)); - assertEquals(flagNoVersion, flagNoVersion.updateFlag(flagLowVersion)); assertEquals(flagSameVersion, flagLowVersion.updateFlag(flagSameVersion)); assertEquals(flagHighVersion, flagHighVersion.updateFlag(flagLowVersion)); assertEquals(flagHighVersion, flagLowVersion.updateFlag(flagHighVersion)); diff --git a/testharness-suppressions.txt b/testharness-suppressions.txt index 8667c169..e69de29b 100644 --- a/testharness-suppressions.txt +++ b/testharness-suppressions.txt @@ -1,16 +0,0 @@ -# sc-159880 (sdk bug) - NullPointerException on null variation id -# sc-160002 (sdk bug) - doesn't support initialRetryDelayMs -streaming/updates/ -# sc-159579 (test harness bug) - `device` and `os` user properties -streaming/requests/user properties/GET -streaming/requests/user properties/REPORT -polling/requests/user properties/GET -polling/requests/user properties/REPORT -# sc-159583 (sdk bug) - `anonymous: false` in events -# sc-159579 (test harness bug) - `device` and `os` user properties -events/user properties/ -# sc-159578 (sdk bug) - trailing slashes not handled properly -streaming/requests/URL path is computed correctly/base URI has a trailing slash/GET -streaming/requests/URL path is computed correctly/base URI has a trailing slash/REPORT -polling/requests/URL path is computed correctly/base URI has a trailing slash/GET -polling/requests/URL path is computed correctly/base URI has a trailing slash/REPORT