From 367551a030276318b60acae8859b9f13a4fb5328 Mon Sep 17 00:00:00 2001 From: LaunchDarklyReleaseBot <86431345+LaunchDarklyReleaseBot@users.noreply.github.com> Date: Fri, 2 Dec 2022 15:20:14 -0800 Subject: [PATCH] prepare 3.3.0 release (#198) * 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) * Bump Gradle, Android Gradle Plugin, and Dexcount Gradle * Use the latest 7.1.1 version * 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 * use com.launchdarkly.logging with Timber adapter (#235) * rm unused plugin * clean up leftover polling alarms * don't use connection pool/keep-alive for polling requests * add sub-configuration builder for events * diagnosticRecordingInterval should also be part of the new builder * misc fixes * remove deprecated usages & unused imports * misc fixes * revert unnecessary change * doc comments * add configuration builders for polling/streaming * fix polling mode initialization * fix diagnostic event properties * fix logic for diagnostic recording interval * fix tests * fix defaulting logic * fix test * add configuration builder for HTTP * improve tests * test cleanup * fix test * add configuration builder for service endpoints * misc fixes * disable diagnostic events if analytics events are disabled * deprecations * don't keep summary event counters in SharedPreferences * don't create a summary event if there's no data * rm duplicated lines * use regular in-memory storage for summary events (customer-reported performance issue) (#279) * don't keep summary event counters in SharedPreferences * don't create a summary event if there's no data * fix doc comment * fix @since Co-authored-by: Gavin Whelan 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 --- .../launchdarkly/sdktest/Representations.java | 9 +- .../launchdarkly/sdktest/SdkClientEntity.java | 89 ++- .../com/launchdarkly/sdktest/TestService.java | 3 + .../sdk/android/ConnectivityManagerTest.java | 21 +- .../android/DiagnosticEventProcessorTest.java | 36 +- .../sdk/android/DiagnosticEventTest.java | 289 +++++-- .../launchdarkly/sdk/android/EventTest.java | 101 ++- .../sdk/android/LDClientTest.java | 17 +- .../sdk/android/LDConfigTest.java | 83 +- .../launchdarkly/sdk/android/TestUtil.java | 7 + .../sdk/android/ClientContextImpl.java | 105 +++ .../launchdarkly/sdk/android/Components.java | 160 ++++ .../sdk/android/ComponentsImpl.java | 250 ++++++ .../sdk/android/ConnectivityManager.java | 25 +- .../sdk/android/DefaultEventProcessor.java | 117 ++- .../sdk/android/DiagnosticEvent.java | 74 +- .../sdk/android/DiagnosticEventProcessor.java | 32 +- .../sdk/android/EventProcessor.java | 7 - .../sdk/android/HttpFeatureFlagFetcher.java | 34 +- .../launchdarkly/sdk/android/LDClient.java | 107 +-- .../launchdarkly/sdk/android/LDConfig.java | 756 ++++++++++++++---- .../com/launchdarkly/sdk/android/LDUtil.java | 42 +- .../sdk/android/PollingUpdater.java | 2 +- .../sdk/android/StandardEndpoints.java | 55 ++ .../sdk/android/StreamUpdateProcessor.java | 35 +- .../integrations/EventProcessorBuilder.java | 178 +++++ .../HttpConfigurationBuilder.java | 99 +++ .../PollingDataSourceBuilder.java | 77 ++ .../integrations/ServiceEndpointsBuilder.java | 242 ++++++ .../StreamingDataSourceBuilder.java | 70 ++ .../sdk/android/subsystems/ClientContext.java | 151 ++++ .../subsystems/ComponentConfigurer.java | 20 + .../sdk/android/subsystems/DataSource.java | 42 + .../subsystems/DiagnosticDescription.java | 27 + .../android/subsystems/EventProcessor.java | 116 +++ .../android/subsystems/HttpConfiguration.java | 87 ++ .../android/subsystems/ServiceEndpoints.java | 53 ++ 37 files changed, 3105 insertions(+), 513 deletions(-) create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java delete mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventProcessor.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ServiceEndpointsBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingDataSourceBuilder.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ComponentConfigurer.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSource.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DiagnosticDescription.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HttpConfiguration.java create mode 100644 launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ServiceEndpoints.java diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java index 23ed8695..5a2a0c81 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/Representations.java @@ -31,11 +31,12 @@ public static class SdkConfigParams { SdkConfigEventParams events; SdkConfigTagParams tags; SdkConfigClientSideParams clientSide; + SdkConfigServiceEndpointParams serviceEndpoints; } public static class SdkConfigStreamParams { String baseUri; - long initialRetryDelayMs; + Long initialRetryDelayMs; } public static class SdkConfigPollParams { @@ -58,6 +59,12 @@ public static class SdkConfigTagParams { String applicationVersion; } + public static class SdkConfigServiceEndpointParams { + String streaming; + String polling; + String events; + } + public static class SdkConfigClientSideParams { LDUser initialUser; boolean autoAliasingOptOut; 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 f375fdd0..c62c90a7 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/SdkClientEntity.java @@ -4,11 +4,15 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.EvaluationDetail; import com.launchdarkly.sdk.LDValue; -import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.Components; import com.launchdarkly.sdk.android.LaunchDarklyException; import com.launchdarkly.sdk.android.LDClient; import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; import com.launchdarkly.sdktest.Representations.AliasEventParams; import com.launchdarkly.sdktest.Representations.CommandParams; import com.launchdarkly.sdktest.Representations.CreateInstanceParams; @@ -26,8 +30,6 @@ import java.io.IOException; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -197,60 +199,67 @@ private LDConfig buildSdkConfig(SdkConfigParams params, LDLogAdapter logAdapter, builder.mobileKey(params.credential); builder.logAdapter(logAdapter).loggerName(tag + ".sdk"); - if (params.streaming != null) { - builder.stream(true); - if (params.streaming.baseUri != null) { - builder.streamUri(Uri.parse(params.streaming.baseUri)); - } - // TODO: initialRetryDelayMs? - } + ServiceEndpointsBuilder endpoints = Components.serviceEndpoints(); - // The only time we should turn _off_ streaming is if polling is configured but NOT streaming - if (params.streaming == null && params.polling != null) { - builder.stream(false); + if (params.polling != null) { + // Note that this property can be set even if streaming is enabled + endpoints.polling(params.polling.baseUri); } - if (params.polling != null) { - if (params.polling.baseUri != null) { - builder.pollUri(Uri.parse(params.polling.baseUri)); - } + if (params.polling != null && params.streaming == null) { + PollingDataSourceBuilder pollingBuilder = Components.pollingDataSource(); if (params.polling.pollIntervalMs != null) { - builder.backgroundPollingIntervalMillis(params.polling.pollIntervalMs.intValue()); + pollingBuilder.pollIntervalMillis(params.polling.pollIntervalMs.intValue()); } + builder.dataSource(pollingBuilder); + } else if (params.streaming != null) { + endpoints.streaming(params.streaming.baseUri); + StreamingDataSourceBuilder streamingBuilder = Components.streamingDataSource(); + if (params.streaming.initialRetryDelayMs != null) { + streamingBuilder.initialReconnectDelayMillis(params.streaming.initialRetryDelayMs.intValue()); + } + builder.dataSource(streamingBuilder); } - if (params.events != null) { + if (params.events == null) { + builder.events(Components.noEvents()); + } else { builder.diagnosticOptOut(!params.events.enableDiagnostics); - builder.inlineUsersInEvents(params.events.inlineUsers); - - if (params.events.baseUri != null) { - builder.eventsUri(Uri.parse(params.events.baseUri)); - } + endpoints.events(params.events.baseUri); + EventProcessorBuilder eventsBuilder = Components.sendEvents() + .allAttributesPrivate(params.events.allAttributesPrivate) + .inlineUsers(params.events.inlineUsers); if (params.events.capacity > 0) { - builder.eventsCapacity(params.events.capacity); + eventsBuilder.capacity(params.events.capacity); } if (params.events.flushIntervalMs != null) { - builder.eventsFlushIntervalMillis(params.events.flushIntervalMs.intValue()); - } - if (params.events.allAttributesPrivate) { - builder.allAttributesPrivate(); - } - if (params.events.flushIntervalMs != null) { - builder.eventsFlushIntervalMillis(params.events.flushIntervalMs.intValue()); + eventsBuilder.flushIntervalMillis(params.events.flushIntervalMs.intValue()); } if (params.events.globalPrivateAttributes != null) { - String[] attrNames = params.events.globalPrivateAttributes; - List privateAttributes = new ArrayList<>(); - for (String a : attrNames) { - privateAttributes.add(UserAttribute.forName(a)); - } - builder.privateAttributes((UserAttribute[]) privateAttributes.toArray(new UserAttribute[]{})); + eventsBuilder.privateAttributes(params.events.globalPrivateAttributes); } + builder.events(eventsBuilder); } - // TODO: disable events if no params.events + builder.autoAliasingOptOut(params.clientSide.autoAliasingOptOut); builder.evaluationReasons(params.clientSide.evaluationReasons); - builder.useReport(params.clientSide.useReport); + builder.http( + Components.httpConfiguration().useReport(params.clientSide.useReport) + ); + + if (params.serviceEndpoints != null) { + if (params.serviceEndpoints.streaming != null) { + endpoints.streaming(params.serviceEndpoints.streaming); + } + if (params.serviceEndpoints.polling != null) { + endpoints.polling(params.serviceEndpoints.polling); + } + if (params.serviceEndpoints.events != null) { + endpoints.events(params.serviceEndpoints.events); + } + } + + builder.serviceEndpoints(endpoints); return builder.build(); } diff --git a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java index 00cdae1c..e4bd626e 100644 --- a/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java +++ b/contract-tests/src/main/java/com/launchdarkly/sdktest/TestService.java @@ -7,6 +7,7 @@ import com.google.gson.JsonSyntaxException; import com.launchdarkly.logging.LDLogAdapter; import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdktest.Representations.CommandParams; import com.launchdarkly.sdktest.Representations.CreateInstanceParams; import com.launchdarkly.sdktest.Representations.Status; @@ -28,6 +29,7 @@ public class TestService extends NanoHTTPD { private static final String[] CAPABILITIES = new String[]{ "client-side", "mobile", + "service-endpoints", "singleton", "strongly-typed", }; @@ -84,6 +86,7 @@ public Response serve(IHTTPSession session) { return newFixedLengthResponse(Response.Status.BAD_REQUEST, NanoHTTPD.MIME_PLAINTEXT, "Invalid JSON Format\n"); } catch (Exception e) { logger.error("Exception when handling request: {} {} - {}", method.name(), session.getUri(), e); + logger.error(LogValues.exceptionTrace(e)); return newFixedLengthResponse(Response.Status.INTERNAL_ERROR, NanoHTTPD.MIME_PLAINTEXT, e.toString()); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java index 23973cca..07dbf7b1 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/ConnectivityManagerTest.java @@ -1,10 +1,11 @@ package com.launchdarkly.sdk.android; +import static com.launchdarkly.sdk.android.TestUtil.simpleClientContext; + import android.app.Application; import android.content.Context; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.Uri; import android.os.Build; import android.os.StrictMode; import android.os.StrictMode.ThreadPolicy; @@ -17,7 +18,11 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.ConnectionInformation.ConnectionMode; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import org.easymock.Capture; import org.easymock.EasyMockRule; @@ -127,12 +132,20 @@ private void createTestManager(boolean setOffline, boolean streaming, boolean ba LDConfig config = new LDConfig.Builder() .mobileKey("test-mobile-key") .offline(setOffline) - .stream(streaming) .disableBackgroundUpdating(backgroundDisabled) - .streamUri(streamUri != null ? Uri.parse(streamUri) : Uri.parse(mockStreamServer.url("/").toString())) + .serviceEndpoints( + Components.serviceEndpoints().streaming( + streamUri != null ? streamUri : mockStreamServer.url("/").toString() + ) + ) .build(); - connectivityManager = new ConnectivityManager(app, config, eventProcessor, userManager, "default", + ComponentConfigurer dataSourceConfigurer = streaming ? + Components.streamingDataSource() : Components.pollingDataSource(); + DataSource dataSourceConfig = dataSourceConfigurer.build(null); + HttpConfiguration httpConfig = simpleClientContext(config).getHttp(); + connectivityManager = new ConnectivityManager(app, config, dataSourceConfig, httpConfig, + eventProcessor, userManager, "default", null, null, LDLogger.none()); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java index 403dbd28..0ba89174 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventProcessorTest.java @@ -1,6 +1,6 @@ package com.launchdarkly.sdk.android; -import android.net.Uri; +import static com.launchdarkly.sdk.android.TestUtil.simpleClientContext; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -20,6 +20,7 @@ import static junit.framework.Assert.assertEquals; import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; @RunWith(AndroidJUnit4.class) public class DiagnosticEventProcessorTest { @@ -46,11 +47,12 @@ public void defaultDiagnosticRequest() throws InterruptedException { OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); LDConfig ldConfig = new LDConfig.Builder() .mobileKey("test-mobile-key") - .eventsUri(Uri.parse(mockEventsServer.url("").toString())) + .serviceEndpoints(Components.serviceEndpoints().events(mockEventsServer.url("").toString())) .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, - ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); + HttpConfiguration httpConfig = simpleClientContext(ldConfig).getHttp(); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, httpConfig, + diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -72,13 +74,13 @@ public void defaultDiagnosticRequestIncludingWrapper() throws InterruptedExcepti OkHttpClient okHttpClient = new OkHttpClient.Builder().build(); LDConfig ldConfig = new LDConfig.Builder() .mobileKey("test-mobile-key") - .eventsUri(Uri.parse(mockEventsServer.url("").toString())) - .wrapperName("ReactNative") - .wrapperVersion("1.0.0") + .serviceEndpoints(Components.serviceEndpoints().events(mockEventsServer.url("").toString())) + .http(Components.httpConfiguration().wrapper("ReactNative", "1.0.0")) .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, - ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); + HttpConfiguration httpConfig = simpleClientContext(ldConfig).getHttp(); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, httpConfig, + diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -99,15 +101,16 @@ public void defaultDiagnosticRequestIncludingAdditionalHeaders() throws Interrup LDConfig ldConfig = new LDConfig.Builder() .mobileKey("test-mobile-key") - .eventsUri(Uri.parse(mockEventsServer.url("").toString())) - .headerTransform(headers -> { + .serviceEndpoints(Components.serviceEndpoints().events(mockEventsServer.url("").toString())) + .http(Components.httpConfiguration().headerTransform(headers -> { headers.put("Proxy-Authorization", "token"); headers.put("Authorization", "foo"); - }) + })) .build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, - ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); + HttpConfiguration httpConfig = simpleClientContext(ldConfig).getHttp(); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, httpConfig, + diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); DiagnosticEvent testEvent = new DiagnosticEvent("test-kind", System.currentTimeMillis(), diagnosticStore.getDiagnosticId()); @@ -128,8 +131,9 @@ public void closeWithoutStart() { LDConfig ldConfig = new LDConfig.Builder().mobileKey("test-mobile-key").build(); DiagnosticStore diagnosticStore = new DiagnosticStore(ApplicationProvider.getApplicationContext(), "test-mobile-key"); - DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, "default", diagnosticStore, - ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); + HttpConfiguration httpConfig = simpleClientContext(ldConfig).getHttp(); + DiagnosticEventProcessor diagnosticEventProcessor = new DiagnosticEventProcessor(ldConfig, httpConfig, + diagnosticStore, ApplicationProvider.getApplicationContext(), okHttpClient, LDLogger.none()); diagnosticEventProcessor.close(); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventTest.java index beffdbb6..eda593fa 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/DiagnosticEventTest.java @@ -6,6 +6,11 @@ import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; import org.junit.Assert; import org.junit.Rule; @@ -22,83 +27,219 @@ public class DiagnosticEventTest { public TimberLoggingRule timberLoggingRule = new TimberLoggingRule(); @Test - public void testDefaultDiagnosticConfiguration() { + public void defaultDiagnosticConfiguration() { LDConfig ldConfig = new LDConfig.Builder().build(); - DiagnosticEvent.DiagnosticConfiguration diagnosticConfiguration = new DiagnosticEvent.DiagnosticConfiguration(ldConfig); - JsonObject diagnosticJson = GsonCache.getGson().toJsonTree(diagnosticConfiguration).getAsJsonObject(); - JsonObject expected = new JsonObject(); - expected.addProperty("allAttributesPrivate", false); - expected.addProperty("backgroundPollingDisabled", false); - expected.addProperty("backgroundPollingIntervalMillis", 3_600_000); - expected.addProperty("connectTimeoutMillis", 10_000); - expected.addProperty("customBaseURI", false); - expected.addProperty("customEventsURI", false); - expected.addProperty("customStreamURI", false); - expected.addProperty("diagnosticRecordingIntervalMillis", 900_000); - expected.addProperty("evaluationReasonsRequested", false); - expected.addProperty("eventsCapacity", 100); - expected.addProperty("eventsFlushIntervalMillis",30_000); - expected.addProperty("inlineUsersInEvents", false); - expected.addProperty("mobileKeyCount", 1); - expected.addProperty("pollingIntervalMillis", 300_000); - expected.addProperty("streamingDisabled", false); - expected.addProperty("useReport", false); - expected.addProperty("maxCachedUsers", 5); - expected.addProperty("autoAliasingOptOut", false); - Assert.assertEquals(expected, diagnosticJson); + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + Assert.assertEquals(expected.build(), diagnosticJson); } @Test - public void testCustomDiagnosticConfiguration() { + public void customDiagnosticConfigurationGeneral() { HashMap secondaryKeys = new HashMap<>(1); secondaryKeys.put("secondary", "key"); LDConfig ldConfig = new LDConfig.Builder() - .allAttributesPrivate() .disableBackgroundUpdating(true) - .backgroundPollingIntervalMillis(900_000) - .connectionTimeoutMillis(5_000) - .pollUri(Uri.parse("https://1.1.1.1")) - .eventsUri(Uri.parse("https://1.1.1.1")) - .streamUri(Uri.parse("https://1.1.1.1")) - .diagnosticRecordingIntervalMillis(1_800_000) .evaluationReasons(true) + .secondaryMobileKeys(secondaryKeys) + .maxCachedUsers(-1) + .autoAliasingOptOut(true) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("backgroundPollingDisabled", true); + expected.put("evaluationReasonsRequested", true); + expected.put("mobileKeyCount", 2); + expected.put("maxCachedUsers", -1); + expected.put("autoAliasingOptOut", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void customDiagnosticConfigurationEvents() { + LDConfig ldConfig = new LDConfig.Builder() + .events( + Components.sendEvents() + .allAttributesPrivate(true) + .capacity(1000) + .diagnosticRecordingIntervalMillis(1_800_000) + .flushIntervalMillis(60_000) + .inlineUsers(true) + ) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("allAttributesPrivate", true); + expected.put("diagnosticRecordingIntervalMillis", 1_800_000); + expected.put("eventsCapacity", 1000); + expected.put("eventsFlushIntervalMillis", 60_000); + expected.put("inlineUsersInEvents", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void customDiagnosticConfigurationStreaming() { + LDConfig ldConfig = new LDConfig.Builder() + .dataSource( + Components.streamingDataSource() + .backgroundPollIntervalMillis(900_000) + .initialReconnectDelayMillis(500) + ) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("backgroundPollingIntervalMillis", 900_000); + expected.put("reconnectTimeMillis", 500); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void customDiagnosticConfigurationPolling() { + LDConfig ldConfig = new LDConfig.Builder() + .dataSource( + Components.pollingDataSource() + .backgroundPollIntervalMillis(900_000) + .pollIntervalMillis(600_000) + ) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaultsWithoutStreaming(); + expected.put("streamingDisabled", true); + expected.put("backgroundPollingIntervalMillis", 900_000); + expected.put("pollingIntervalMillis", 600_000); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void customDiagnosticConfigurationHttp() { + LDConfig ldConfig = new LDConfig.Builder() + .http( + Components.httpConfiguration() + .connectTimeoutMillis(5_000) + .useReport(true) + ) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("connectTimeoutMillis", 5_000); + expected.put("useReport", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void customDiagnosticConfigurationServiceEndpoints() { + LDConfig ldConfig = new LDConfig.Builder() + .serviceEndpoints( + Components.serviceEndpoints() + .streaming("https://1.1.1.1") + .polling("https://1.1.1.1") + .events("https://1.1.1.1") + ) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("customBaseURI", true); + expected.put("customEventsURI", true); + expected.put("customStreamURI", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @SuppressWarnings("deprecation") + @Test + public void customDiagnosticConfigurationEventsWithDeprecatedSetters() { + LDConfig ldConfig = new LDConfig.Builder() + .allAttributesPrivate() + .diagnosticRecordingIntervalMillis(1_800_000) .eventsCapacity(1000) .eventsFlushIntervalMillis(60_000) .inlineUsersInEvents(true) - .secondaryMobileKeys(secondaryKeys) - .pollingIntervalMillis(600_000) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("allAttributesPrivate", true); + expected.put("diagnosticRecordingIntervalMillis", 1_800_000); + expected.put("eventsCapacity", 1000); + expected.put("eventsFlushIntervalMillis", 60_000); + expected.put("inlineUsersInEvents", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @SuppressWarnings("deprecation") + @Test + public void customDiagnosticConfigurationStreamingWithDeprecatedSetters() { + LDConfig ldConfig = new LDConfig.Builder() + .backgroundPollingIntervalMillis(900_000) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("backgroundPollingIntervalMillis", 900_000); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @SuppressWarnings("deprecation") + @Test + public void customDiagnosticConfigurationPollingWithDeprecatedSetters() { + LDConfig ldConfig = new LDConfig.Builder() .stream(false) + .backgroundPollingIntervalMillis(900_000) + .pollingIntervalMillis(600_000) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaultsWithoutStreaming(); + expected.put("streamingDisabled", true); + expected.put("backgroundPollingIntervalMillis", 900_000); + expected.put("pollingIntervalMillis", 600_000); + + // When using the deprecated setters only, there is an extra defaulting rule that causes + // the event flush interval to match the polling interval if not otherwise specified. + expected.put("eventsFlushIntervalMillis", 600_000); + + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @SuppressWarnings("deprecation") + @Test + public void customDiagnosticConfigurationHttpWithDeprecatedSetters() { + LDConfig ldConfig = new LDConfig.Builder() + .connectionTimeoutMillis(5_000) .useReport(true) - .maxCachedUsers(-1) - .autoAliasingOptOut(true) .build(); - DiagnosticEvent.DiagnosticConfiguration diagnosticConfiguration = new DiagnosticEvent.DiagnosticConfiguration(ldConfig); - JsonObject diagnosticJson = GsonCache.getGson().toJsonTree(diagnosticConfiguration).getAsJsonObject(); - JsonObject expected = new JsonObject(); - expected.addProperty("allAttributesPrivate", true); - expected.addProperty("backgroundPollingDisabled", true); - expected.addProperty("backgroundPollingIntervalMillis", 900_000); - expected.addProperty("connectTimeoutMillis", 5_000); - expected.addProperty("customBaseURI", true); - expected.addProperty("customEventsURI", true); - expected.addProperty("customStreamURI", true); - expected.addProperty("diagnosticRecordingIntervalMillis", 1_800_000); - expected.addProperty("evaluationReasonsRequested", true); - expected.addProperty("eventsCapacity", 1000); - expected.addProperty("eventsFlushIntervalMillis",60_000); - expected.addProperty("inlineUsersInEvents", true); - expected.addProperty("mobileKeyCount", 2); - expected.addProperty("pollingIntervalMillis", 600_000); - expected.addProperty("streamingDisabled", true); - expected.addProperty("useReport", true); - expected.addProperty("maxCachedUsers", -1); - expected.addProperty("autoAliasingOptOut", true); - Assert.assertEquals(expected, diagnosticJson); + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("connectTimeoutMillis", 5_000); + expected.put("useReport", true); + Assert.assertEquals(expected.build(), diagnosticJson); } + @SuppressWarnings("deprecation") @Test - public void testStatisticsEventSerialization(){ + public void customDiagnosticConfigurationServiceEndpointsWithDeprecatedSetters() { + LDConfig ldConfig = new LDConfig.Builder() + .streamUri(Uri.parse("https://1.1.1.1")) + .pollUri(Uri.parse("https://1.1.1.1")) + .eventsUri(Uri.parse("https://1.1.1.1")) + .build(); + + LDValue diagnosticJson = DiagnosticEvent.makeConfigurationInfo(ldConfig); + ObjectBuilder expected = makeExpectedDefaults(); + expected.put("customBaseURI", true); + expected.put("customEventsURI", true); + expected.put("customStreamURI", true); + Assert.assertEquals(expected.build(), diagnosticJson); + } + + @Test + public void statisticsEventSerialization() { DiagnosticEvent.Statistics statisticsEvent = new DiagnosticEvent.Statistics(2_000, new DiagnosticId("testid", "testkey"), 1_000, 5, 100, Collections.singletonList(new DiagnosticEvent.StreamInit(100, 50, false))); @@ -122,4 +263,36 @@ public void testStatisticsEventSerialization(){ expected.add("streamInits", expectedStreamInits); Assert.assertEquals(expected, diagnosticJson); } + + private static ObjectBuilder makeExpectedDefaultsWithoutStreaming() { + ObjectBuilder expected = LDValue.buildObject(); + expected.put("allAttributesPrivate", false); + expected.put("autoAliasingOptOut", false); + expected.put("backgroundPollingDisabled", false); + expected.put("backgroundPollingIntervalMillis", + LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS); + expected.put("connectTimeoutMillis", + HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS); + expected.put("customBaseURI", false); + expected.put("customEventsURI", false); + expected.put("customStreamURI", false); + expected.put("diagnosticRecordingIntervalMillis", + EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS); + expected.put("evaluationReasonsRequested", false); + expected.put("eventsCapacity", EventProcessorBuilder.DEFAULT_CAPACITY); + expected.put("eventsFlushIntervalMillis", + EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL_MILLIS); + expected.put("inlineUsersInEvents", false); + expected.put("maxCachedUsers", 5); + expected.put("mobileKeyCount", 1); + expected.put("streamingDisabled", false); + expected.put("useReport", false); + return expected; + } + + private static ObjectBuilder makeExpectedDefaults() { + return makeExpectedDefaultsWithoutStreaming() + .put("reconnectTimeMillis", + StreamingDataSourceBuilder.DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS); + } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EventTest.java index a45d4b36..f4c0366b 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/EventTest.java @@ -12,12 +12,6 @@ import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.ObjectBuilder; import com.launchdarkly.sdk.UserAttribute; -import com.launchdarkly.sdk.android.AliasEvent; -import com.launchdarkly.sdk.android.CustomEvent; -import com.launchdarkly.sdk.android.Event; -import com.launchdarkly.sdk.android.FeatureRequestEvent; -import com.launchdarkly.sdk.android.GenericEvent; -import com.launchdarkly.sdk.android.LDConfig; import org.junit.Test; import org.junit.runner.RunWith; @@ -35,7 +29,6 @@ public class EventTest { @Test public void testPrivateAttributesAreConcatenated() { - LDUser.Builder builder = new LDUser.Builder("1") .privateAvatar("privateAvatar") .privateFirstName("privateName") @@ -47,12 +40,15 @@ public void testPrivateAttributesAreConcatenated() { LDUser user = builder.build(); LDConfig config = new LDConfig.Builder() - .privateAttributes(UserAttribute.EMAIL, UserAttribute.forName("Value2")) + .events( + Components.sendEvents() + .privateAttributes(UserAttribute.EMAIL, UserAttribute.forName("Value2")) + ) .build(); final Event event = new GenericEvent("kind1", "key1", user); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonArray privateAttrs = jsonElement.getAsJsonObject().get("user").getAsJsonObject().getAsJsonArray("privateAttrs"); assertNotNull(jsonElement); @@ -83,7 +79,7 @@ public void testPrivateAttributes() { Event event = new GenericEvent("kind1", "key1", user); - JsonObject userJson = config.getFilteredEventGson().toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); + JsonObject userJson = config.filteredEventGson.toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); JsonArray privateAttrs = userJson.getAsJsonArray("privateAttrs"); assertEquals(1, privateAttrs.size()); @@ -102,12 +98,15 @@ public void testRegularAttributesAreFilteredWithPrivateAttributes() { LDUser user = builder.build(); LDConfig config = new LDConfig.Builder() - .privateAttributes(UserAttribute.AVATAR) + .events( + Components.sendEvents() + .privateAttributes(UserAttribute.AVATAR) + ) .build(); Event event = new GenericEvent("kind1", "key1", user); - JsonObject userJson = config.getFilteredEventGson().toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); + JsonObject userJson = config.filteredEventGson.toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); JsonArray privateAttrs = userJson.getAsJsonArray("privateAttrs"); assertEquals(1, privateAttrs.size()); @@ -130,7 +129,7 @@ public void testPrivateAttributesJsonOnLDUserObject() { Gson gson = new Gson(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject userEval = gson.fromJson(gson.toJson(user), JsonObject.class); JsonArray privateAttrs = jsonElement.getAsJsonObject().getAsJsonObject("user").getAsJsonArray("privateAttrs"); @@ -154,12 +153,14 @@ public void testRegularAttributesAreFilteredWithAllAttributesPrivate() { LDUser user = builder.build(); LDConfig config = new LDConfig.Builder() - .allAttributesPrivate() + .events( + Components.sendEvents().allAttributesPrivate(true) + ) .build(); Event event = new GenericEvent("kind1", "key1", user); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonArray privateAttrs = jsonElement.getAsJsonObject().getAsJsonObject("user").getAsJsonArray("privateAttrs"); assertNotNull(user); @@ -180,12 +181,14 @@ public void testKeyAndAnonymousAreNotFilteredWithAllAttributesPrivate() { .build(); LDConfig config = new LDConfig.Builder() - .allAttributesPrivate() + .events( + Components.sendEvents().allAttributesPrivate(true) + ) .build(); Event event = new GenericEvent("kind1", "key1", user); - JsonObject userJson = config.getFilteredEventGson().toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); + JsonObject userJson = config.filteredEventGson.toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); JsonArray privateAttrs = userJson.getAsJsonArray("privateAttrs"); assertEquals(1, privateAttrs.size()); @@ -301,7 +304,7 @@ public void testCustomEventWithoutDataSerialization() { CustomEvent event = new CustomEvent("key1", new LDUser.Builder("a").build(), null, null, false); LDConfig config = new LDConfig.Builder().build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject eventObject = jsonElement.getAsJsonObject(); assertEquals(4, eventObject.size()); @@ -316,7 +319,7 @@ public void testCustomEventWithNullValueDataSerialization() { CustomEvent event = new CustomEvent("key1", new LDUser.Builder("a").build(), LDValue.ofNull(), null, false); LDConfig config = new LDConfig.Builder().build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject eventObject = jsonElement.getAsJsonObject(); assertEquals(4, eventObject.size()); @@ -331,7 +334,7 @@ public void testCustomEventWithDataSerialization() { CustomEvent event = new CustomEvent("key1", new LDUser.Builder("a").build(), LDValue.of("abc"), null, false); LDConfig config = new LDConfig.Builder().build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject eventObject = jsonElement.getAsJsonObject(); assertEquals(5, eventObject.size()); @@ -347,7 +350,7 @@ public void testCustomEventWithMetricSerialization() { CustomEvent event = new CustomEvent("key1", new LDUser.Builder("a").build(), null, 5.5, false); LDConfig config = new LDConfig.Builder().build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject eventObject = jsonElement.getAsJsonObject(); assertEquals(5, eventObject.size()); @@ -366,7 +369,7 @@ public void testCustomEventWithDataAndMetricSerialization() { CustomEvent event = new CustomEvent("key1", new LDUser.Builder("a").build(), objVal, -10.0, false); LDConfig config = new LDConfig.Builder().build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(event); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(event); JsonObject eventObject = jsonElement.getAsJsonObject(); assertEquals(6, eventObject.size()); @@ -409,9 +412,59 @@ public void reasonIsSerialized() { LDConfig config = new LDConfig.Builder() .build(); - JsonElement jsonElement = config.getFilteredEventGson().toJsonTree(hasReasonEvent); + JsonElement jsonElement = config.filteredEventGson.toJsonTree(hasReasonEvent); - JsonElement expected = config.getFilteredEventGson().fromJson("{\"kind\":\"FALLTHROUGH\"}", JsonElement.class); + JsonElement expected = config.filteredEventGson.fromJson("{\"kind\":\"FALLTHROUGH\"}", JsonElement.class); assertEquals(expected, jsonElement.getAsJsonObject().get("reason")); } + + @SuppressWarnings("deprecation") + @Test + public void deprecatedAllAttributesPrivateConfigBuilderMethod() { + LDUser.Builder builder = new LDUser.Builder("1") + .avatar("avatarValue") + .custom("value1", "123") + .email("email@server.net"); + + LDUser user = builder.build(); + + LDConfig config = new LDConfig.Builder() + .allAttributesPrivate() + .build(); + + Event event = new GenericEvent("kind1", "key1", user); + + JsonObject userJson = config.filteredEventGson.toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); + JsonArray privateAttrs = userJson.getAsJsonArray("privateAttrs"); + + assertEquals(3, privateAttrs.size()); + + assertTrue(privateAttrs.contains(new JsonPrimitive(UserAttribute.AVATAR.getName()))); + assertTrue(privateAttrs.contains(new JsonPrimitive(UserAttribute.EMAIL.getName()))); + assertTrue(privateAttrs.contains(new JsonPrimitive("value1"))); + } + + @SuppressWarnings("deprecation") + @Test + public void deprecatedPrivateAttributesConfigBuilderMethod() { + LDUser.Builder builder = new LDUser.Builder("1") + .avatar("avatarValue") + .custom("value1", "123") + .email("email@server.net"); + + LDUser user = builder.build(); + + LDConfig config = new LDConfig.Builder() + .privateAttributes(UserAttribute.AVATAR) + .build(); + + Event event = new GenericEvent("kind1", "key1", user); + + JsonObject userJson = config.filteredEventGson.toJsonTree(event).getAsJsonObject().getAsJsonObject("user"); + JsonArray privateAttrs = userJson.getAsJsonArray("privateAttrs"); + + assertEquals(1, privateAttrs.size()); + assertEquals("avatar", privateAttrs.get(0).getAsString()); + assertNull(userJson.getAsJsonPrimitive("avatar")); + } } 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 818a8efc..f7d2dcaf 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 @@ -1,7 +1,6 @@ package com.launchdarkly.sdk.android; import android.app.Application; -import android.net.Uri; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -632,10 +631,12 @@ public void additionalHeadersIncludedInEventsRequest() throws IOException, Inter // Enqueue a successful empty response mockEventsServer.enqueue(new MockResponse()); - LDConfig ldConfig = baseConfigBuilder(mockEventsServer).headerTransform(headers -> { - headers.put("Proxy-Authorization", "token"); - headers.put("Authorization", "foo"); - }).build(); + LDConfig ldConfig = baseConfigBuilder(mockEventsServer) + .http(Components.httpConfiguration().headerTransform(headers -> { + headers.put("Proxy-Authorization", "token"); + headers.put("Authorization", "foo"); + })) + .build(); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { client.blockingFlush(); } @@ -654,7 +655,7 @@ public void testEventBufferFillsUp() throws IOException, InterruptedException { mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer) - .eventsCapacity(1) + .events(Components.sendEvents().capacity(1)) .build(); // Don't wait as we are not set offline @@ -675,7 +676,7 @@ private Event[] getEventsFromLastRequest(MockWebServer server, int expectedCount RecordedRequest r = server.takeRequest(); assertEquals("POST", r.getMethod()); assertEquals("/mobile", r.getPath()); - assertEquals(LDConfig.AUTH_SCHEME + mobileKey, r.getHeader("Authorization")); + assertEquals(LDUtil.AUTH_SCHEME + mobileKey, r.getHeader("Authorization")); String body = r.getBody().readUtf8(); System.out.println(body); Event[] events = TestUtil.getEventDeserializerGson().fromJson(body, Event[].class); @@ -690,6 +691,6 @@ private LDConfig.Builder baseConfigBuilder(MockWebServer server) { return new LDConfig.Builder() .mobileKey(mobileKey) .diagnosticOptOut(true) - .eventsUri(Uri.parse(baseUrl.toString())); + .serviceEndpoints(Components.serviceEndpoints().events(baseUrl.toString())); } } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDConfigTest.java index d0788d51..7baaf1e6 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/LDConfigTest.java @@ -1,10 +1,16 @@ package com.launchdarkly.sdk.android; +import static com.launchdarkly.sdk.android.TestUtil.simpleClientContext; + import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.gson.JsonElement; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import org.junit.Rule; import org.junit.Test; @@ -23,6 +29,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +@SuppressWarnings("deprecation") @RunWith(AndroidJUnit4.class) public class LDConfigTest { @@ -39,13 +46,13 @@ public void testBuilderDefaults() { assertEquals(LDConfig.DEFAULT_EVENTS_URI, config.getEventsUri()); assertEquals(LDConfig.DEFAULT_STREAM_URI, config.getStreamUri()); - assertEquals(LDConfig.DEFAULT_CONNECTION_TIMEOUT_MILLIS, config.getConnectionTimeoutMillis()); - assertEquals(LDConfig.DEFAULT_EVENTS_CAPACITY, config.getEventsCapacity()); - assertEquals(LDConfig.DEFAULT_FLUSH_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS, config.getDiagnosticRecordingIntervalMillis()); + assertEquals(HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS, config.getConnectionTimeoutMillis()); + assertEquals(EventProcessorBuilder.DEFAULT_CAPACITY, config.getEventsCapacity()); + assertEquals(EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getPollingIntervalMillis()); + assertEquals(EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS, config.getDiagnosticRecordingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); + assertEquals(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); assertFalse(config.isDisableBackgroundPolling()); assertNull(config.getMobileKey()); @@ -67,24 +74,24 @@ public void testBuilderStreamDisabled() { assertFalse(config.isStream()); assertFalse(config.isOffline()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getPollingIntervalMillis()); + assertEquals(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); } @Test public void testBuilderStreamDisabledCustomIntervals() { LDConfig config = new LDConfig.Builder() .stream(false) - .pollingIntervalMillis(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS + 1) - .backgroundPollingIntervalMillis(LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS + 2) + .pollingIntervalMillis(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS + 1) + .backgroundPollingIntervalMillis(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS + 2) .build(); assertFalse(config.isStream()); assertFalse(config.isOffline()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS + 1, config.getPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS + 2, config.getBackgroundPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS + 1, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS + 1, config.getPollingIntervalMillis()); + assertEquals(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS + 2, config.getBackgroundPollingIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS + 1, config.getEventsFlushIntervalMillis()); } @Test @@ -97,58 +104,58 @@ public void testBuilderStreamDisabledBackgroundUpdatingDisabled() { assertFalse(config.isStream()); assertFalse(config.isOffline()); assertTrue(config.isDisableBackgroundPolling()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getPollingIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); } @Test public void testBuilderStreamDisabledPollingIntervalBelowMinimum() { LDConfig config = new LDConfig.Builder() .stream(false) - .pollingIntervalMillis(LDConfig.MIN_POLLING_INTERVAL_MILLIS - 1) + .pollingIntervalMillis(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS - 1) .build(); assertFalse(config.isStream()); assertFalse(config.isOffline()); assertFalse(config.isDisableBackgroundPolling()); - assertEquals(LDConfig.MIN_POLLING_INTERVAL_MILLIS, config.getPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); - assertEquals(LDConfig.MIN_POLLING_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getPollingIntervalMillis()); + assertEquals(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); } @Test public void testBuilderStreamDisabledBackgroundPollingIntervalBelowMinimum() { LDConfig config = new LDConfig.Builder() .stream(false) - .backgroundPollingIntervalMillis(LDConfig.MIN_BACKGROUND_POLLING_INTERVAL_MILLIS - 1) + .backgroundPollingIntervalMillis(LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS - 1) .build(); assertFalse(config.isStream()); assertFalse(config.isOffline()); assertFalse(config.isDisableBackgroundPolling()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getPollingIntervalMillis()); - assertEquals(LDConfig.MIN_BACKGROUND_POLLING_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); - assertEquals(LDConfig.DEFAULT_POLLING_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getPollingIntervalMillis()); + assertEquals(LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS, config.getBackgroundPollingIntervalMillis()); + assertEquals(PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, config.getEventsFlushIntervalMillis()); } @Test public void testBuilderDiagnosticRecordingInterval() { LDConfig config = new LDConfig.Builder() - .diagnosticRecordingIntervalMillis(LDConfig.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS + 1) + .diagnosticRecordingIntervalMillis(EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS + 1) .build(); assertFalse(config.getDiagnosticOptOut()); - assertEquals(LDConfig.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS + 1, config.getDiagnosticRecordingIntervalMillis()); + assertEquals(EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS + 1, config.getDiagnosticRecordingIntervalMillis()); } @Test public void testBuilderDiagnosticRecordingIntervalBelowMinimum() { LDConfig config = new LDConfig.Builder() - .diagnosticRecordingIntervalMillis(LDConfig.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS - 1) + .diagnosticRecordingIntervalMillis(EventProcessorBuilder.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS - 1) .build(); assertFalse(config.getDiagnosticOptOut()); - assertEquals(LDConfig.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS, config.getDiagnosticRecordingIntervalMillis()); + assertEquals(EventProcessorBuilder.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS, config.getDiagnosticRecordingIntervalMillis()); } @Test @@ -254,19 +261,20 @@ Map headersToMap(Headers headers) { } @Test - public void headersForEnvironment() { + public void makeRequestHeaders() { LDConfig config = new LDConfig.Builder().mobileKey("test-key").build(); - Map headers = headersToMap(config.headersForEnvironment(LDConfig.primaryEnvironmentName, null)); + HttpConfiguration httpConfig = simpleClientContext(config).getHttp(); + Map headers = headersToMap(LDUtil.makeRequestHeaders(httpConfig, null)); assertEquals(2, headers.size()); - assertEquals(LDConfig.USER_AGENT_HEADER_VALUE, headers.get("user-agent")); + assertEquals(LDUtil.USER_AGENT_HEADER_VALUE, headers.get("user-agent")); assertEquals("api_key test-key", headers.get("authorization")); // Additional headers extend/replace defaults HashMap additional = new HashMap<>(); additional.put("Authorization", "other-key"); additional.put("Proxy-Authorization", "token"); - headers = headersToMap(config.headersForEnvironment(LDConfig.primaryEnvironmentName, additional)); + headers = headersToMap(LDUtil.makeRequestHeaders(httpConfig, additional)); assertEquals(3, headers.size()); - assertEquals(LDConfig.USER_AGENT_HEADER_VALUE, headers.get("user-agent")); + assertEquals(LDUtil.USER_AGENT_HEADER_VALUE, headers.get("user-agent")); assertEquals("other-key", headers.get("authorization")); assertEquals("token", headers.get("proxy-authorization")); // Also should not modify the given additional headers @@ -286,10 +294,11 @@ public void headersForEnvironmentWithTransform() { headers.put("New", "value"); }) .build(); + HttpConfiguration httpConfig = simpleClientContext(config).getHttp(); - expected.put("User-Agent", LDConfig.USER_AGENT_HEADER_VALUE); + expected.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); expected.put("Authorization", "api_key test-key"); - Map headers = headersToMap(config.headersForEnvironment(LDConfig.primaryEnvironmentName, null)); + Map headers = headersToMap(LDUtil.makeRequestHeaders(httpConfig, null)); assertEquals(2, headers.size()); assertEquals("api_key test-key, more", headers.get("authorization")); assertEquals("value", headers.get("new")); @@ -298,7 +307,7 @@ public void headersForEnvironmentWithTransform() { additional.put("Authorization", "other-key"); additional.put("Proxy-Authorization", "token"); expected.putAll(additional); - headers = headersToMap(config.headersForEnvironment(LDConfig.primaryEnvironmentName, additional)); + headers = headersToMap(LDUtil.makeRequestHeaders(httpConfig, additional)); assertEquals(3, headers.size()); assertEquals("other-key, more", headers.get("authorization")); assertEquals("token", headers.get("proxy-authorization")); @@ -324,7 +333,7 @@ public void keyShouldNeverBeRemoved() { LDUser user = new LDUser.Builder("myUserKey").email("weShouldNotFindThis@test.com").build(); - JsonElement elem = config.getFilteredEventGson().toJsonTree(user).getAsJsonObject(); + JsonElement elem = config.filteredEventGson.toJsonTree(user).getAsJsonObject(); assertNotNull(elem); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestUtil.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestUtil.java index 955e8504..92993c84 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestUtil.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/sdk/android/TestUtil.java @@ -11,6 +11,7 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; +import com.launchdarkly.logging.LDLogger; import com.launchdarkly.sdk.android.AliasEvent; import com.launchdarkly.sdk.android.CustomEvent; import com.launchdarkly.sdk.android.Event; @@ -18,10 +19,16 @@ import com.launchdarkly.sdk.android.IdentifyEvent; import com.launchdarkly.sdk.android.LDConfig; import com.launchdarkly.sdk.android.SummaryEvent; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import java.lang.reflect.Type; public class TestUtil { + public static ClientContext simpleClientContext(LDConfig config) { + return ClientContextImpl.fromConfig(null, config, config.getMobileKey(), + "", null, null, null, LDLogger.none()); + } private static class EventDeserializer implements JsonDeserializer { @Override diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java new file mode 100644 index 00000000..718b8dee --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java @@ -0,0 +1,105 @@ +package com.launchdarkly.sdk.android; + +import android.app.Application; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; + +import okhttp3.OkHttpClient; + +/** + * This package-private subclass of {@link ClientContext} contains additional non-public SDK objects + * that may be used by our internal components. + *

+ * The reason for using this mechanism, instead of just passing those objects directly as constructor + * parameters, is that some SDK components are pluggable-- that is, they are implementations of a + * public interface that a customer could implement themselves, and they are instantiated via a + * standard factory method, which always takes a {@link ClientContext} parameter. Customer code can + * only see the public properties of {@link ClientContext}, but our own code can see the + * package-private properties, which they can do by calling {@code ClientContextImpl.get(ClientContext)} + * to make sure that what they have is really a {@code ClientContextImpl} (as opposed to some other + * implementation of {@link ClientContext}, which might have been created for instance in application + * test code). + *

+ * Any attempt by SDK components to access an object that would normally be provided by the SDK, + * but that has not been set, will cause an immediate unchecked exception. This would only happen if + * components were being used outside of the SDK client in test code that did not correctly set + * these properties. + */ +final class ClientContextImpl extends ClientContext { + private final DiagnosticStore diagnosticStore; + private final OkHttpClient sharedEventClient; + private final SummaryEventStore summaryEventStore; + + ClientContextImpl( + ClientContext base, + DiagnosticStore diagnosticStore, + OkHttpClient sharedEventClient, + SummaryEventStore summaryEventStore + ) { + super(base); + this.diagnosticStore = diagnosticStore; + this.sharedEventClient = sharedEventClient; + this.summaryEventStore = summaryEventStore; + } + + static ClientContextImpl fromConfig( + Application application, + LDConfig config, + String mobileKey, + String environmentName, + DiagnosticStore diagnosticStore, + OkHttpClient sharedEventClient, + SummaryEventStore summaryEventStore, + LDLogger logger + ) { + ClientContext minimalContext = new ClientContext(null, mobileKey, logger, config, + environmentName, config.isEvaluationReasons(), null, config.isOffline(), + config.serviceEndpoints); + HttpConfiguration httpConfig = config.http.build(minimalContext); + ClientContext baseClientContext = new ClientContext( + application, + mobileKey, + logger, + config, + environmentName, + config.isEvaluationReasons(), + httpConfig, + config.isOffline(), + config.serviceEndpoints + ); + return new ClientContextImpl(baseClientContext, diagnosticStore, sharedEventClient, summaryEventStore); + } + + public static ClientContextImpl get(ClientContext context) { + if (context instanceof ClientContextImpl) { + return (ClientContextImpl)context; + } + return new ClientContextImpl(context, null, null, null); + } + + public DiagnosticStore getDiagnosticStore() { + return diagnosticStore; + } + + public OkHttpClient getSharedEventClient() { + throwExceptionIfNull(sharedEventClient); + return sharedEventClient; + } + + public SummaryEventStore getSummaryEventStore() { + throwExceptionIfNull(summaryEventStore); + return summaryEventStore; + } + + private static void throwExceptionIfNull(Object o) { + if (o == null) { + throw new IllegalStateException( + "Attempted to use an SDK component without the necessary dependencies from LDClient; " + + " this should never happen unless an application has tried to construct the" + + " component directly outside of normal SDK usage" + ); + } + } +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java new file mode 100644 index 00000000..771a18a0 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/Components.java @@ -0,0 +1,160 @@ +package com.launchdarkly.sdk.android; + +import static com.launchdarkly.sdk.android.ComponentsImpl.NULL_EVENT_PROCESSOR_FACTORY; + +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; + +/** + * Provides configurable factories for the standard implementations of LaunchDarkly component interfaces. + *

+ * Some of the configuration options in {@link LDConfig.Builder} affect the entire SDK, but others are + * specific to one area of functionality, such as how the SDK receives feature flag updates or processes + * analytics events. For the latter, the standard way to specify a configuration is to call one of the + * static methods in {@link Components}, apply any desired configuration change to the object that that + * method returns, and then use the corresponding method in {@link LDConfig.Builder} to use that + * configured component in the SDK. + * + * @since 3.3.0 + */ +public abstract class Components { + private Components() {} + + /** + * Returns a configuration builder for the SDK's networking configuration. + *

+ * Passing this to {@link LDConfig.Builder#http(ComponentConfigurer)} applies this configuration + * to all HTTP/HTTPS requests made by the SDK. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .http(
+     *              Components.httpConfiguration()
+     *                  .connectTimeoutMillis(3000)
+     *                  .proxyHostAndPort("my-proxy", 8080)
+     *         )
+     *         .build();
+     * 
+ * + * @return a factory object + * @see LDConfig.Builder#http(ComponentConfigurer) + */ + public static HttpConfigurationBuilder httpConfiguration() { + return new ComponentsImpl.HttpConfigurationBuilderImpl(); + } + + /** + * Returns a configuration object that disables analytics events. + *

+ * Passing this to {@link LDConfig.Builder#events(ComponentConfigurer)} causes the SDK + * to discard all analytics events and not send them to LaunchDarkly, regardless of any other configuration. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .events(Components.noEvents())
+     *         .build();
+     * 
+ * + * @return a configuration object + * @see #sendEvents() + * @see LDConfig.Builder#events(ComponentConfigurer) + */ + public static ComponentConfigurer noEvents() { + return NULL_EVENT_PROCESSOR_FACTORY; + } + + /** + * Returns a configuration builder for using polling mode to get feature flag data. + *

+ * By default, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. To use the + * default behavior, you do not need to call this method. However, if you want to customize the behavior of + * the connection, call this method to obtain a builder, change its properties with the + * {@link PollingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(ComponentConfigurer)}: + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .dataSource(Components.pollingDataSource().initialReconnectDelayMillis(500))
+     *         .build();
+     * 
+ *

+ * Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting + * and completely disable network requests. + * + * @return a builder for setting streaming connection properties + * @see LDConfig.Builder#dataSource(ComponentConfigurer) + */ + public static PollingDataSourceBuilder pollingDataSource() { + return new ComponentsImpl.PollingDataSourceBuilderImpl(); + } + + /** + * Returns a configuration builder for analytics event delivery. + *

+ * The default configuration has events enabled with default settings. If you want to + * customize this behavior, call this method to obtain a builder, change its properties + * with the {@link EventProcessorBuilder} properties, and pass it to {@link LDConfig.Builder#events(ComponentConfigurer)}: + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .events(Components.sendEvents().capacity(500).flushIntervalMillis(2000))
+     *         .build();
+     * 
+ * To completely disable sending analytics events, use {@link #noEvents()} instead. + *

+ * Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting + * and completely disable network requests. + * + * @return a builder for setting event-related options + * @see #noEvents() + * @see LDConfig.Builder#events(ComponentConfigurer) + */ + public static EventProcessorBuilder sendEvents() { + return new ComponentsImpl.EventProcessorBuilderImpl(); + } + + /** + * Returns a builder for configuring custom service URIs. + *

+ * Passing this to {@link LDConfig.Builder#serviceEndpoints(com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder)}, + * after setting any desired properties on the builder, applies this configuration to the SDK. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .serviceEndpoints(
+     *             Components.serviceEndpoints()
+     *                 .relayProxy("http://my-relay-hostname:80")
+     *         )
+     *         .build();
+     * 
+ * + * @return a builder object + * @see LDConfig.Builder#serviceEndpoints(com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder) + * @since 3.3.0 + */ + public static ServiceEndpointsBuilder serviceEndpoints() { + return new ComponentsImpl.ServiceEndpointsBuilderImpl(); + } + + /** + * Returns a configuration builder for using streaming mode to get feature flag data. + *

+ * By default, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. To use the + * default behavior, you do not need to call this method. However, if you want to customize the behavior of + * the connection, call this method to obtain a builder, change its properties with the + * {@link StreamingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(ComponentConfigurer)}: + *


+     *     LDConfig config = new LDConfig.Builder()
+     *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
+     *         .build();
+     * 
+ *

+ * Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting + * and completely disable network requests. + * + * @return a builder for setting streaming connection properties + * @see LDConfig.Builder#dataSource(ComponentConfigurer) + */ + public static StreamingDataSourceBuilder streamingDataSource() { + return new ComponentsImpl.StreamingDataSourceBuilderImpl(); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java new file mode 100644 index 00000000..ca19f5bf --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ComponentsImpl.java @@ -0,0 +1,250 @@ +package com.launchdarkly.sdk.android; + +import android.net.Uri; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.DiagnosticDescription; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.android.subsystems.ServiceEndpoints; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * This class contains the package-private implementations of component factories and builders whose + * public factory methods are in {@link Components}. + */ +abstract class ComponentsImpl { + private ComponentsImpl() { + } + + static final ComponentConfigurer NULL_EVENT_PROCESSOR_FACTORY = new ComponentConfigurer() { + public EventProcessor build(ClientContext clientContext) { + return NullEventProcessor.INSTANCE; + } + }; + + /** + * Stub implementation of {@link EventProcessor} for when we don't want to send any events. + */ + static final class NullEventProcessor implements EventProcessor { + static final NullEventProcessor INSTANCE = new NullEventProcessor(); + + private NullEventProcessor() {} + + @Override + public void flush() {} + + @Override + public void blockingFlush() {} + + @Override + public void start() {} + + @Override + public void stop() {} + + @Override + public void close() {} + + @Override + public void recordEvaluationEvent(LDUser user, String flagKey, int flagVersion, int variation, LDValue value, + EvaluationReason reason, LDValue defaultValue, boolean requireFullEvent, + Long debugEventsUntilDate) {} + + @Override + public void recordIdentifyEvent(LDUser user) {} + + @Override + public void recordCustomEvent(LDUser user, String eventKey, LDValue data, Double metricValue) {} + + @Override + public void recordAliasEvent(LDUser user, LDUser previousUser) {} + + @Override + public void setOffline(boolean offline) {} + } + + static final class EventProcessorBuilderImpl extends EventProcessorBuilder + implements DiagnosticDescription { + // see comments in LDConfig constructor regarding the purpose of these package-private getters + boolean isAllAttributesPrivate() { + return allAttributesPrivate; + } + + int getDiagnosticRecordingIntervalMillis() { return diagnosticRecordingIntervalMillis; } + + Set getPrivateAttributes() { + return privateAttributes; + } + + @Override + public EventProcessor build(ClientContext clientContext) { + ClientContextImpl clientContextImpl = ClientContextImpl.get(clientContext); + URI eventsUri = StandardEndpoints.selectBaseUri(clientContext.getServiceEndpoints().getEventsBaseUri(), + StandardEndpoints.DEFAULT_EVENTS_BASE_URI, "events", clientContext.getBaseLogger()); + return new DefaultEventProcessor( + clientContext.getApplication(), + clientContext.getConfig(), + clientContext.getHttp(), + eventsUri, + clientContextImpl.getSummaryEventStore(), + clientContext.getEnvironmentName(), + clientContext.isInitiallySetOffline(), + capacity, + flushIntervalMillis, + inlineUsers, + clientContextImpl.getDiagnosticStore(), + clientContextImpl.getSharedEventClient(), + clientContext.getBaseLogger() + ); + } + + @Override + public LDValue describeConfiguration(ClientContext clientContext) { + return LDValue.buildObject() + .put("allAttributesPrivate", allAttributesPrivate) + .put("diagnosticRecordingIntervalMillis", diagnosticRecordingIntervalMillis) + .put("eventsCapacity", capacity) + .put("diagnosticRecordingIntervalMillis", diagnosticRecordingIntervalMillis) + .put("eventsFlushIntervalMillis", flushIntervalMillis) + .put("inlineUsersInEvents", inlineUsers) + .build(); + } + } + + static final class HttpConfigurationBuilderImpl extends HttpConfigurationBuilder + implements DiagnosticDescription { + @Override + public HttpConfiguration build(ClientContext clientContext) { + LDLogger logger = clientContext.getBaseLogger(); + // Build the default headers + Map headers = new HashMap<>(); + headers.put("Authorization", LDUtil.AUTH_SCHEME + clientContext.getMobileKey()); + headers.put("User-Agent", LDUtil.USER_AGENT_HEADER_VALUE); + if (wrapperName != null) { + String wrapperId = wrapperVersion == null ? wrapperName : (wrapperName + "/" + wrapperVersion); + headers.put("X-LaunchDarkly-Wrapper", wrapperId); + } + + return new HttpConfiguration( + connectTimeoutMillis, + headers, + headerTransform, + useReport + ); + } + + @Override + public LDValue describeConfiguration(ClientContext clientContext) { + return LDValue.buildObject() + .put("connectTimeoutMillis", connectTimeoutMillis) + .put("useReport", useReport) + .build(); + } + } + + static final class PollingDataSourceBuilderImpl extends PollingDataSourceBuilder + implements DiagnosticDescription { + @Override + public DataSource build(ClientContext clientContext) { + return new DataSourceImpl(true, backgroundPollIntervalMillis, 0, + pollIntervalMillis); + } + + @Override + public LDValue describeConfiguration(ClientContext clientContext) { + return LDValue.buildObject() + .put("streamingDisabled", true) + .put("backgroundPollingIntervalMillis", backgroundPollIntervalMillis) + .put("pollingIntervalMillis", pollIntervalMillis) + .build(); + } + } + + static final class ServiceEndpointsBuilderImpl extends ServiceEndpointsBuilder { + @Override + public ServiceEndpoints build() { + // If *any* custom URIs have been set, then we do not want to use default values for any that were not set, + // so we will leave those null. That way, if we decide later on (in other component factories, such as + // EventProcessorBuilder) that we are actually interested in one of these values, and we + // see that it is null, we can assume that there was a configuration mistake and log an + // error. + if (streamingBaseUri == null && pollingBaseUri == null && eventsBaseUri == null) { + return new ServiceEndpoints( + StandardEndpoints.DEFAULT_STREAMING_BASE_URI, + StandardEndpoints.DEFAULT_POLLING_BASE_URI, + StandardEndpoints.DEFAULT_EVENTS_BASE_URI + ); + } + return new ServiceEndpoints(streamingBaseUri, pollingBaseUri, eventsBaseUri); + } + } + + static final class StreamingDataSourceBuilderImpl extends StreamingDataSourceBuilder + implements DiagnosticDescription { + @Override + public DataSource build(ClientContext clientContext) { + return new DataSourceImpl(false, backgroundPollIntervalMillis, + initialReconnectDelayMillis, 0); + } + + @Override + public LDValue describeConfiguration(ClientContext clientContext) { + return LDValue.buildObject() + .put("streamingDisabled", false) + .put("backgroundPollingIntervalMillis", backgroundPollIntervalMillis) + .put("reconnectTimeMillis", initialReconnectDelayMillis) + .build(); + } + } + + private static final class DataSourceImpl implements DataSource { + private final boolean streamingDisabled; + private final int backgroundPollIntervalMillis; + private final int initialReconnectDelayMillis; + private final int pollIntervalMillis; + + DataSourceImpl( + boolean streamingDisabled, + int backgroundPollIntervalMillis, + int initialReconnectDelayMillis, + int pollIntervalMillis + ) { + this.streamingDisabled = streamingDisabled; + this.backgroundPollIntervalMillis = backgroundPollIntervalMillis; + this.initialReconnectDelayMillis = initialReconnectDelayMillis; + this.pollIntervalMillis = pollIntervalMillis; + } + + public boolean isStreamingDisabled() { + return streamingDisabled; + } + + public int getBackgroundPollIntervalMillis() { + return backgroundPollIntervalMillis; + } + + public int getInitialReconnectDelayMillis() { + return initialReconnectDelayMillis; + } + + public int getPollIntervalMillis() { + return pollIntervalMillis; + } + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java index 3e75c1b3..ed73eb2b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java @@ -7,8 +7,11 @@ import androidx.annotation.NonNull; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.LogValues; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; +import java.net.URI; import java.util.Calendar; import java.util.TimeZone; @@ -42,6 +45,8 @@ class ConnectivityManager { ConnectivityManager(@NonNull final Application application, @NonNull final LDConfig ldConfig, + @NonNull final DataSource dataSourceConfig, + @NonNull final HttpConfiguration httpConfig, @NonNull final EventProcessor eventProcessor, @NonNull final UserManager userManager, @NonNull final String environmentName, @@ -54,15 +59,23 @@ class ConnectivityManager { this.userManager = userManager; this.environmentName = environmentName; this.logger = logger; - pollingInterval = ldConfig.getPollingIntervalMillis(); + pollingInterval = dataSourceConfig.getPollIntervalMillis(); String prefsKey = LDConfig.SHARED_PREFS_BASE_KEY + ldConfig.getMobileKeys().get(environmentName) + "-connectionstatus"; stateStore = application.getSharedPreferences(prefsKey, Context.MODE_PRIVATE); connectionInformation = new ConnectionInformationState(); readStoredConnectionState(); setOffline = ldConfig.isOffline(); + final URI streamUri = dataSourceConfig.isStreamingDisabled() ? null : + StandardEndpoints.selectBaseUri(ldConfig.serviceEndpoints.getStreamingBaseUri(), + StandardEndpoints.DEFAULT_STREAMING_BASE_URI, "streaming", logger); + backgroundMode = ldConfig.isDisableBackgroundPolling() ? ConnectionMode.BACKGROUND_DISABLED : ConnectionMode.BACKGROUND_POLLING; - foregroundMode = ldConfig.isStream() ? ConnectionMode.STREAMING : ConnectionMode.POLLING; + foregroundMode = dataSourceConfig.isStreamingDisabled() ? ConnectionMode.POLLING : ConnectionMode.STREAMING; + + // Currently the background polling interval is owned statically by PollingUpdater, even + // though it is configured in our per-instance DataSource. + PollingUpdater.setBackgroundPollingIntervalMillis(dataSourceConfig.getBackgroundPollIntervalMillis()); throttler = new Throttler(() -> { synchronized (ConnectivityManager.this) { @@ -125,8 +138,9 @@ public void onError(Throwable e) { } }; - streamUpdateProcessor = ldConfig.isStream() ? new StreamUpdateProcessor(ldConfig, userManager, environmentName, - diagnosticStore, monitor, logger) : null; + streamUpdateProcessor = dataSourceConfig.isStreamingDisabled() ? null : + new StreamUpdateProcessor(ldConfig, dataSourceConfig, httpConfig, streamUri, + userManager, environmentName, diagnosticStore, monitor, logger); } boolean isInitialized() { @@ -336,6 +350,7 @@ synchronized void shutdown() { synchronized void setOnline() { if (setOffline) { setOffline = false; + eventProcessor.setOffline(false); startUp(null); } } 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 9940f55e..5312c5a9 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 @@ -1,12 +1,13 @@ package com.launchdarkly.sdk.android; import android.content.Context; +import android.net.Uri; import androidx.annotation.NonNull; -import androidx.annotation.VisibleForTesting; import java.io.Closeable; import java.io.IOException; +import java.net.URI; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -21,6 +22,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import okhttp3.OkHttpClient; @@ -33,7 +35,11 @@ import static com.launchdarkly.sdk.android.LDUtil.isHttpErrorRecoverable; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.LogValues; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; class DefaultEventProcessor implements EventProcessor, Closeable { private static final HashMap baseEventHeaders = new HashMap() {{ @@ -46,19 +52,42 @@ class DefaultEventProcessor implements EventProcessor, Closeable { private final OkHttpClient client; private final Context context; private final LDConfig config; + private final HttpConfiguration httpConfig; private final String environmentName; + private final Uri eventsUri; + private final int flushIntervalMillis; + private final boolean inlineUsers; private ScheduledExecutorService scheduler; private final SummaryEventStore summaryEventStore; + private final AtomicBoolean offline = new AtomicBoolean(); private long currentTimeMs = System.currentTimeMillis(); private DiagnosticStore diagnosticStore; private final LDLogger logger; - DefaultEventProcessor(Context context, LDConfig config, SummaryEventStore summaryEventStore, String environmentName, - final DiagnosticStore diagnosticStore, final OkHttpClient sharedClient, LDLogger logger) { + DefaultEventProcessor( + Context context, + LDConfig config, + HttpConfiguration httpConfig, + URI eventsUri, + SummaryEventStore summaryEventStore, + String environmentName, + boolean initiallyOffline, + int capacity, + int flushIntervalMillis, + boolean inlineUsers, + final DiagnosticStore diagnosticStore, + final OkHttpClient sharedClient, + LDLogger logger + ) { this.context = context; this.config = config; + this.httpConfig = httpConfig; + this.eventsUri = Uri.parse(eventsUri.toString()); + this.offline.set(initiallyOffline); this.environmentName = environmentName; - this.queue = new ArrayBlockingQueue<>(config.getEventsCapacity()); + this.flushIntervalMillis = flushIntervalMillis; + this.inlineUsers = inlineUsers; + this.queue = new ArrayBlockingQueue<>(capacity); this.consumer = new Consumer(config); this.summaryEventStore = summaryEventStore; this.client = sharedClient; @@ -81,7 +110,7 @@ public Thread newThread(@NonNull Runnable r) { } }); - scheduler.scheduleAtFixedRate(consumer, config.getEventsFlushIntervalMillis(), config.getEventsFlushIntervalMillis(), TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(consumer, flushIntervalMillis, flushIntervalMillis, TimeUnit.MILLISECONDS); } } @@ -92,8 +121,71 @@ public void stop() { } } - public boolean sendEvent(Event e) { - return queue.offer(e); + public void recordEvaluationEvent( + LDUser user, + String flagKey, + int flagVersion, + int variation, + LDValue value, + EvaluationReason reason, + LDValue defaultValue, + boolean requireFullEvent, + Long debugEventsUntilDate + ) { + boolean needEvent = false, isDebug = false; + if (requireFullEvent) { + needEvent = true; + } else if (debugEventsUntilDate != null) { + long serverTimeMs = getCurrentTimeMs(); + if (debugEventsUntilDate > System.currentTimeMillis() && debugEventsUntilDate > serverTimeMs) { + needEvent = isDebug = true; + } + } + if (needEvent) { + sendEvent(new FeatureRequestEvent(flagKey, user, value, defaultValue, + flagVersion < 0 ? null : Integer.valueOf(flagVersion), + variation < 0 ? null : Integer.valueOf(variation), + reason, isDebug || inlineUsers, isDebug)); + } + } + + public void recordIdentifyEvent( + LDUser user + ) { + sendEvent(new IdentifyEvent(user)); + } + + public void recordCustomEvent( + LDUser user, + String eventKey, + LDValue data, + Double metricValue + ) { + sendEvent(new CustomEvent(eventKey, user, data, metricValue, inlineUsers)); + } + + public void recordAliasEvent( + LDUser user, + LDUser previousUser + ) { + sendEvent(new AliasEvent(user, previousUser)); + } + + public void setOffline(boolean offline) { + this.offline.set(offline); + } + + private void sendEvent(Event e) { + if (offline.get()) { + return; + } + boolean processed = queue.offer(e); + if (!processed) { + logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); + if (diagnosticStore != null) { + diagnosticStore.incrementDroppedEventCount(); + } + } } @Override @@ -108,8 +200,7 @@ public void flush() { } } - @VisibleForTesting - void blockingFlush() { + public void blockingFlush() { consumer.run(); } @@ -149,9 +240,9 @@ synchronized void flush() { } private void postEvents(List events) { - String content = config.getFilteredEventGson().toJson(events); + String content = config.filteredEventGson.toJson(events); String eventPayloadId = UUID.randomUUID().toString(); - String url = config.getEventsUri().buildUpon().appendPath("mobile").build().toString(); + String url = eventsUri.buildUpon().appendPath("mobile").build().toString(); HashMap baseHeadersForRequest = new HashMap<>(); baseHeadersForRequest.put("X-LaunchDarkly-Payload-ID", eventPayloadId); baseHeadersForRequest.putAll(baseEventHeaders); @@ -168,7 +259,7 @@ private void postEvents(List events) { } Request request = new Request.Builder().url(url) - .headers(config.headersForEnvironment(environmentName, baseHeadersForRequest)) + .headers(LDUtil.makeRequestHeaders(httpConfig, baseHeadersForRequest)) .post(RequestBody.create(content, JSON)) .build(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEvent.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEvent.java index 462d8965..8a150628 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEvent.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEvent.java @@ -2,6 +2,11 @@ import android.os.Build; +import com.launchdarkly.sdk.LDValue; +import com.launchdarkly.sdk.ObjectBuilder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DiagnosticDescription; + import java.util.List; @SuppressWarnings({"unused", "FieldCanBeLocal"}) // fields are for JSON serialization only @@ -25,58 +30,43 @@ static class DiagnosticPlatform { } } - static class DiagnosticConfiguration { - private final boolean customBaseURI; - private final boolean customEventsURI; - private final boolean customStreamURI; - private final int eventsCapacity; - private final int connectTimeoutMillis; - private final long eventsFlushIntervalMillis; - private final boolean streamingDisabled; - private final boolean allAttributesPrivate; - private final long pollingIntervalMillis; - private final long backgroundPollingIntervalMillis; - private final boolean inlineUsersInEvents; - private final boolean useReport; - private final boolean backgroundPollingDisabled; - private final boolean evaluationReasonsRequested; - private final int mobileKeyCount; - private final int diagnosticRecordingIntervalMillis; - private final int maxCachedUsers; - private final boolean autoAliasingOptOut; - - DiagnosticConfiguration(LDConfig config) { - this.customBaseURI = !LDConfig.DEFAULT_POLL_URI.equals(config.getPollUri()); - this.customEventsURI = !LDConfig.DEFAULT_EVENTS_URI.equals(config.getEventsUri()); - this.customStreamURI = !LDConfig.DEFAULT_STREAM_URI.equals(config.getStreamUri()); - this.eventsCapacity = config.getEventsCapacity(); - this.connectTimeoutMillis = config.getConnectionTimeoutMillis(); - this.eventsFlushIntervalMillis = config.getEventsFlushIntervalMillis(); - this.streamingDisabled = !config.isStream(); - this.allAttributesPrivate = config.allAttributesPrivate(); - this.pollingIntervalMillis = config.getPollingIntervalMillis(); - this.backgroundPollingIntervalMillis = config.getBackgroundPollingIntervalMillis(); - this.inlineUsersInEvents = config.inlineUsersInEvents(); - this.useReport = config.isUseReport(); - this.backgroundPollingDisabled = config.isDisableBackgroundPolling(); - this.evaluationReasonsRequested = config.isEvaluationReasons(); - this.mobileKeyCount = config.getMobileKeys().size(); - this.diagnosticRecordingIntervalMillis = config.getDiagnosticRecordingIntervalMillis(); - this.maxCachedUsers = config.getMaxCachedUsers(); - this.autoAliasingOptOut = config.isAutoAliasingOptOut(); - } + static LDValue makeConfigurationInfo(LDConfig config) { + ObjectBuilder builder = LDValue.buildObject() + .put("customBaseURI", + !StandardEndpoints.DEFAULT_POLLING_BASE_URI.equals(config.serviceEndpoints.getPollingBaseUri())) + .put("customEventsURI", + !StandardEndpoints.DEFAULT_EVENTS_BASE_URI.equals(config.serviceEndpoints.getEventsBaseUri())) + .put("customStreamURI", + !StandardEndpoints.DEFAULT_STREAMING_BASE_URI.equals(config.serviceEndpoints.getStreamingBaseUri())) + .put("backgroundPollingDisabled", config.isDisableBackgroundPolling()) + .put("evaluationReasonsRequested", config.isEvaluationReasons()) + .put("mobileKeyCount", config.getMobileKeys().size()) + .put("maxCachedUsers", config.getMaxCachedUsers()) + .put("autoAliasingOptOut", config.isAutoAliasingOptOut()); + mergeComponentProperties(builder, config.events); + mergeComponentProperties(builder, config.dataSource); + mergeComponentProperties(builder, config.http); + return builder.build(); + } + private static void mergeComponentProperties(ObjectBuilder builder, ComponentConfigurer componentConfigurer) { + if (componentConfigurer instanceof DiagnosticDescription) { + LDValue description = ((DiagnosticDescription)componentConfigurer).describeConfiguration(null); + for (String key: description.keys()) { + builder.put(key, description.get(key)); + } + } } static class Init extends DiagnosticEvent { final DiagnosticSdk sdk; - final DiagnosticConfiguration configuration; + final LDValue configuration; final DiagnosticPlatform platform = new DiagnosticPlatform(); Init(long creationDate, DiagnosticId diagnosticId, LDConfig config) { super("diagnostic-init", creationDate, diagnosticId); this.sdk = new DiagnosticSdk(config); - this.configuration = new DiagnosticConfiguration(config); + this.configuration = makeConfigurationInfo(config); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java index e0ac19dc..1580fb3f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DiagnosticEventProcessor.java @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.android; import android.content.Context; +import android.net.Uri; import androidx.annotation.NonNull; @@ -22,6 +23,7 @@ import com.launchdarkly.logging.LDLogger; import com.launchdarkly.logging.LogValues; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; class DiagnosticEventProcessor { private static final HashMap baseDiagnosticHeaders = new HashMap() {{ @@ -29,18 +31,26 @@ class DiagnosticEventProcessor { }}; private final OkHttpClient client; - private final LDConfig config; - private final String environment; + private final HttpConfiguration httpConfig; + private final int diagnosticRecordingIntervalMillis; + private final Uri eventsUri; private final DiagnosticStore diagnosticStore; private final ThreadFactory diagnosticThreadFactory; private final Context context; private final LDLogger logger; private ScheduledExecutorService executorService; - DiagnosticEventProcessor(LDConfig config, String environment, final DiagnosticStore diagnosticStore, Context context, - OkHttpClient sharedClient, LDLogger logger) { - this.config = config; - this.environment = environment; + DiagnosticEventProcessor( + LDConfig config, + HttpConfiguration httpConfig, + final DiagnosticStore diagnosticStore, + Context context, + OkHttpClient sharedClient, + LDLogger logger + ) { + this.httpConfig = httpConfig; + this.diagnosticRecordingIntervalMillis = config.getDiagnosticRecordingIntervalMillis(); + this.eventsUri = Uri.parse(config.serviceEndpoints.getEventsBaseUri().toString()); this.diagnosticStore = diagnosticStore; this.client = sharedClient; this.context = context; @@ -95,14 +105,14 @@ private void enqueueEvent() { void startScheduler() { if (executorService == null) { - long initialDelay = config.getDiagnosticRecordingIntervalMillis() - (System.currentTimeMillis() - diagnosticStore.getDataSince()); - long safeDelay = Math.min(Math.max(initialDelay, 0), config.getDiagnosticRecordingIntervalMillis()); + long initialDelay = diagnosticRecordingIntervalMillis - (System.currentTimeMillis() - diagnosticStore.getDataSince()); + long safeDelay = Math.min(Math.max(initialDelay, 0), diagnosticRecordingIntervalMillis); executorService = Executors.newSingleThreadScheduledExecutor(diagnosticThreadFactory); executorService.scheduleAtFixedRate( this::enqueueEvent, safeDelay, - config.getDiagnosticRecordingIntervalMillis(), + diagnosticRecordingIntervalMillis, TimeUnit.MILLISECONDS ); } @@ -127,8 +137,8 @@ void sendDiagnosticEventSync(DiagnosticEvent diagnosticEvent) { String content = GsonCache.getGson().toJson(diagnosticEvent); Request request = new Request.Builder() - .url(config.getEventsUri().buildUpon().appendEncodedPath("mobile/events/diagnostic").build().toString()) - .headers(config.headersForEnvironment(environment, baseDiagnosticHeaders)) + .url(eventsUri.buildUpon().appendEncodedPath("mobile/events/diagnostic").build().toString()) + .headers(LDUtil.makeRequestHeaders(httpConfig, baseDiagnosticHeaders)) .post(RequestBody.create(content, JSON)).build(); logger.debug("Posting diagnostic event to {} with body {}", request.url(), content); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventProcessor.java deleted file mode 100644 index 99762aff..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/EventProcessor.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.launchdarkly.sdk.android; - -interface EventProcessor { - void start(); - void stop(); - void flush(); -} 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 568c47c8..d8fd1e71 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 @@ -8,17 +8,16 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import java.io.File; import java.io.IOException; -import java.util.concurrent.TimeUnit; +import java.net.URI; import okhttp3.Cache; import okhttp3.Call; import okhttp3.Callback; -import okhttp3.ConnectionPool; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; @@ -34,21 +33,30 @@ class HttpFeatureFlagFetcher implements FeatureFetcher { private static final int MAX_CACHE_SIZE_BYTES = 500_000; private final LDConfig config; + private final HttpConfiguration httpConfig; + private final Uri pollUri; private final String environmentName; private final Context context; private final OkHttpClient client; private final LDLogger logger; - static HttpFeatureFlagFetcher newInstance(Context context, LDConfig config, String environmentName, LDLogger logger) { - return new HttpFeatureFlagFetcher(context, config, environmentName, logger); - } - - private HttpFeatureFlagFetcher(Context context, LDConfig config, String environmentName, LDLogger logger) { + HttpFeatureFlagFetcher( + Context context, + LDConfig config, + HttpConfiguration httpConfig, + String environmentName, + LDLogger logger + ) { this.config = config; + this.httpConfig = httpConfig; this.environmentName = environmentName; this.context = context; this.logger = logger; + URI pollUri = StandardEndpoints.selectBaseUri(config.serviceEndpoints.getPollingBaseUri(), + StandardEndpoints.DEFAULT_POLLING_BASE_URI, "polling", logger); + this.pollUri = Uri.parse(pollUri.toString()); + File cacheDir = new File(context.getCacheDir(), "com.launchdarkly.http-cache"); logger.debug("Using cache at: {}", cacheDir.getAbsolutePath()); @@ -62,7 +70,7 @@ private HttpFeatureFlagFetcher(Context context, LDConfig config, String environm public synchronized void fetch(LDUser user, final LDUtil.ResultCallback callback) { if (user != null && isClientConnected(context, environmentName)) { - final Request request = config.isUseReport() + final Request request = httpConfig.isUseReport() ? getReportRequest(user) : getDefaultRequest(user); @@ -112,19 +120,19 @@ public void onResponse(@NonNull Call call, @NonNull final Response response) { } private Request getDefaultRequest(LDUser user) { - String uri = Uri.withAppendedPath(config.getPollUri(), "msdk/evalx/users/").toString() + + String uri = Uri.withAppendedPath(pollUri, "msdk/evalx/users/").toString() + DefaultUserManager.base64Url(user); if (config.isEvaluationReasons()) { uri += "?withReasons=true"; } logger.debug("Attempting to fetch Feature flags using uri: {}", uri); return new Request.Builder().url(uri) - .headers(config.headersForEnvironment(environmentName, null)) + .headers(LDUtil.makeRequestHeaders(httpConfig, null)) .build(); } private Request getReportRequest(LDUser user) { - String reportUri = Uri.withAppendedPath(config.getPollUri(), "msdk/evalx/user").toString(); + String reportUri = Uri.withAppendedPath(pollUri, "msdk/evalx/user").toString(); if (config.isEvaluationReasons()) { reportUri += "?withReasons=true"; } @@ -133,7 +141,7 @@ private Request getReportRequest(LDUser user) { RequestBody reportBody = RequestBody.create(userJson, JSON); return new Request.Builder().url(reportUri) - .headers(config.headersForEnvironment(environmentName, null)) + .headers(LDUtil.makeRequestHeaders(httpConfig, null)) .method("REPORT", reportBody) .build(); } 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 32c74dc1..d9d75b3e 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 @@ -16,6 +16,10 @@ import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import java.io.Closeable; import java.io.IOException; @@ -59,7 +63,7 @@ public class LDClient implements LDClientInterface, Closeable { private final Application application; private final LDConfig config; private final DefaultUserManager userManager; - private final DefaultEventProcessor eventProcessor; + private final EventProcessor eventProcessor; private final ConnectivityManager connectivityManager; private final DiagnosticEventProcessor diagnosticEventProcessor; private final DiagnosticStore diagnosticStore; @@ -162,14 +166,12 @@ public void onError(Throwable e) { } }; - PollingUpdater.setBackgroundPollingIntervalMillis(config.getBackgroundPollingIntervalMillis()); - user = customizeUser(user); // Start up all instances for (final LDClient instance : instances.values()) { if (instance.connectivityManager.startUp(completeWhenCounterZero)) { - instance.sendEvent(new IdentifyEvent(user)); + instance.eventProcessor.recordIdentifyEvent(user); } } @@ -267,23 +269,44 @@ protected LDClient(final Application application, @NonNull final LDConfig config this.config = config; this.application = application; String sdkKey = config.getMobileKeys().get(environmentName); - FeatureFetcher fetcher = HttpFeatureFlagFetcher.newInstance(application, config, environmentName, logger); - OkHttpClient sharedEventClient = makeSharedEventClient(); - if (config.getDiagnosticOptOut()) { + + // This extra creation of a ClientContext is a temporary workaround for a circular dependency + // in our components: we want the real context to include the SummaryEventStore, but currently + // we can only get that once we have a UserManager, which requires a FeatureFetcher. This will + // be moot in the next major version where the components are better encapsulated. + ClientContext incompleteClientContext = ClientContextImpl.fromConfig(application, config, + sdkKey, environmentName, null, null, null, logger); + HttpConfiguration httpConfig = incompleteClientContext.getHttp(); + + FeatureFetcher fetcher = new HttpFeatureFlagFetcher(application, config, httpConfig, + environmentName, logger); + OkHttpClient sharedEventClient = makeSharedEventClient(httpConfig); + if (config.getDiagnosticOptOut() || (config.events == ComponentsImpl.NULL_EVENT_PROCESSOR_FACTORY)) { this.diagnosticStore = null; this.diagnosticEventProcessor = null; } else { this.diagnosticStore = new DiagnosticStore(application, sdkKey); - this.diagnosticEventProcessor = new DiagnosticEventProcessor(config, environmentName, diagnosticStore, application, + this.diagnosticEventProcessor = new DiagnosticEventProcessor(config, httpConfig, diagnosticStore, application, sharedEventClient, logger); } this.userManager = DefaultUserManager.newInstance(application, fetcher, environmentName, sdkKey, config.getMaxCachedUsers(), logger); - eventProcessor = new DefaultEventProcessor(application, config, userManager.getSummaryEventStore(), environmentName, - diagnosticStore, sharedEventClient, logger); - connectivityManager = new ConnectivityManager(application, config, eventProcessor, userManager, environmentName, - diagnosticEventProcessor, diagnosticStore, logger); + ClientContext clientContext = ClientContextImpl.fromConfig( + application, + config, + sdkKey, + environmentName, + diagnosticStore, + sharedEventClient, + userManager.getSummaryEventStore(), + logger + ); + DataSource dataSource = config.dataSource.build(clientContext); + eventProcessor = config.events.build(clientContext); + connectivityManager = new ConnectivityManager(application, config, dataSource, + clientContext.getHttp(), eventProcessor, userManager, + environmentName, diagnosticEventProcessor, diagnosticStore, logger); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { connectivityReceiver = new ConnectivityReceiver(); @@ -292,10 +315,10 @@ protected LDClient(final Application application, @NonNull final LDConfig config } } - private OkHttpClient makeSharedEventClient() { + private OkHttpClient makeSharedEventClient(HttpConfiguration httpConfig) { return new OkHttpClient.Builder() .connectionPool(new ConnectionPool(1, config.getEventsFlushIntervalMillis() * 2, TimeUnit.MILLISECONDS)) - .connectTimeout(config.getConnectionTimeoutMillis(), TimeUnit.MILLISECONDS) + .connectTimeout(httpConfig.getConnectTimeoutMillis(), TimeUnit.MILLISECONDS) .retryOnConnectionFailure(true) .build(); } @@ -316,7 +339,7 @@ public void track(String eventName) { } private void trackInternal(String eventName, LDValue data, Double metricValue) { - sendEvent(new CustomEvent(eventName, userManager.getCurrentUser(), data, metricValue, config.inlineUsersInEvents())); + eventProcessor.recordCustomEvent(userManager.getCurrentUser(), eventName, data, metricValue); } @Override @@ -351,12 +374,12 @@ private void identifyInternal(@NonNull LDUser user, if (!config.isAutoAliasingOptOut()) { LDUser previousUser = userManager.getCurrentUser(); if (Event.userContextKind(previousUser).equals("anonymousUser") && Event.userContextKind(user).equals("user")) { - sendEvent(new AliasEvent(user, previousUser)); + eventProcessor.recordAliasEvent(user, previousUser); } } userManager.setCurrentUser(user); connectivityManager.reloadUser(onCompleteListener); - sendEvent(new IdentifyEvent(user)); + eventProcessor.recordIdentifyEvent(user); } private Future identifyInstances(@NonNull LDUser user) { @@ -472,7 +495,17 @@ private EvaluationDetail variationDetailInternal(@NonNull String key, @ } else { result = EvaluationDetail.fromValue(value, variation, flag.getReason()); } - sendFlagRequestEvent(key, flag, value, defaultValue, flag.isTrackReason() | needsReason ? result.getReason() : null); + eventProcessor.recordEvaluationEvent( + userManager.getCurrentUser(), + key, + flag.getVersionForEvents(), + flag.getVariation() == null ? -1 : flag.getVariation().intValue(), + value, + flag.isTrackReason() | needsReason ? result.getReason() : null, + defaultValue, + flag.isTrackEvents(), + flag.getDebugEventsUntilDate() + ); } logger.debug("returning variation: {} flagKey: {} user key: {}", result, key, userManager.getCurrentUser().getKey()); @@ -492,7 +525,11 @@ public void close() throws IOException { private void closeInternal() { connectivityManager.shutdown(); - eventProcessor.close(); + try { + eventProcessor.close(); + } catch (IOException e) { + LDUtil.logExceptionAtWarnLevel(logger, e, "Unexpected exception from closing event processor"); + } if (connectivityReceiver != null) { application.unregisterReceiver(connectivityReceiver); @@ -618,7 +655,7 @@ public void unregisterAllFlagsListener(LDAllFlagsListener allFlagsListener) { * @param previousUser The second user */ public void alias(LDUser user, LDUser previousUser) { - sendEvent(new AliasEvent(customizeUser(user), customizeUser(previousUser))); + eventProcessor.recordAliasEvent(customizeUser(user), customizeUser(previousUser)); } private void triggerPoll() { @@ -666,36 +703,6 @@ private void onNetworkConnectivityChange(boolean connectedToInternet) { connectivityManager.onNetworkConnectivityChange(connectedToInternet); } - private void sendFlagRequestEvent(String flagKey, Flag flag, LDValue value, LDValue defaultValue, EvaluationReason reason) { - int version = flag.getVersionForEvents(); - Integer variation = flag.getVariation(); - if (flag.isTrackEvents()) { - sendEvent(new FeatureRequestEvent(flagKey, userManager.getCurrentUser(), value, defaultValue, version, - variation, reason, config.inlineUsersInEvents(), false)); - } else { - Long debugEventsUntilDate = flag.getDebugEventsUntilDate(); - if (debugEventsUntilDate != null) { - long serverTimeMs = eventProcessor.getCurrentTimeMs(); - if (debugEventsUntilDate > System.currentTimeMillis() && debugEventsUntilDate > serverTimeMs) { - sendEvent(new FeatureRequestEvent(flagKey, userManager.getCurrentUser(), value, defaultValue, version, - variation, reason, false, true)); - } - } - } - } - - private void sendEvent(Event event) { - if (!connectivityManager.isOffline()) { - boolean processed = eventProcessor.sendEvent(event); - if (!processed) { - logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); - if (diagnosticStore != null) { - diagnosticStore.incrementDroppedEventCount(); - } - } - } - } - /** * Updates the internal representation of a summary event, either adding a new field or updating the existing count. * Nothing is sent to the server. diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java index e6b1e39c..eb5107b4 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/LDConfig.java @@ -2,8 +2,6 @@ import android.net.Uri; -import androidx.annotation.NonNull; - import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.launchdarkly.logging.LDLogAdapter; @@ -12,17 +10,25 @@ import com.launchdarkly.logging.Logs; import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.UserAttribute; - -import java.util.ArrayList; +import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; +import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; +import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.android.subsystems.ServiceEndpoints; + +import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; -import okhttp3.Headers; import okhttp3.MediaType; /** @@ -30,32 +36,34 @@ * must be constructed with {@link LDConfig.Builder}. */ public class LDConfig { + /** + * The default value for {@link com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder#backgroundPollIntervalMillis(int)} + * and {@link com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder#backgroundPollIntervalMillis(int)}: + * one hour. + */ + public static final int DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS = 3_600_000; + + /** + * The minimum value for {@link com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder#backgroundPollIntervalMillis(int)} + * and {@link com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder#backgroundPollIntervalMillis(int)}: + * 15 minutes. + */ + public static final int MIN_BACKGROUND_POLL_INTERVAL_MILLIS = 900_000; static final String DEFAULT_LOGGER_NAME = "LaunchDarklySdk"; static final LDLogLevel DEFAULT_LOG_LEVEL = LDLogLevel.INFO; static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; - static final String USER_AGENT_HEADER_VALUE = "AndroidClient/" + BuildConfig.VERSION_NAME; - static final String AUTH_SCHEME = "api_key "; static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); static final Gson GSON = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create(); static final String primaryEnvironmentName = "default"; - static final Uri DEFAULT_POLL_URI = Uri.parse("https://clientsdk.launchdarkly.com"); - static final Uri DEFAULT_EVENTS_URI = Uri.parse("https://mobile.launchdarkly.com"); - static final Uri DEFAULT_STREAM_URI = Uri.parse("https://clientstream.launchdarkly.com"); + static final Uri DEFAULT_POLL_URI = Uri.parse(StandardEndpoints.DEFAULT_POLLING_BASE_URI.toString()); + static final Uri DEFAULT_EVENTS_URI = Uri.parse(StandardEndpoints.DEFAULT_EVENTS_BASE_URI.toString()); + static final Uri DEFAULT_STREAM_URI = Uri.parse(StandardEndpoints.DEFAULT_STREAMING_BASE_URI.toString()); - static final int DEFAULT_EVENTS_CAPACITY = 100; static final int DEFAULT_MAX_CACHED_USERS = 5; - static final int DEFAULT_FLUSH_INTERVAL_MILLIS = 30_000; // 30 seconds - static final int DEFAULT_CONNECTION_TIMEOUT_MILLIS = 10_000; // 10 seconds - static final int DEFAULT_POLLING_INTERVAL_MILLIS = 300_000; // 5 minutes - static final int DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS = 3_600_000; // 1 hour - static final int MIN_BACKGROUND_POLLING_INTERVAL_MILLIS = 900_000; // 15 minutes - static final int MIN_POLLING_INTERVAL_MILLIS = 300_000; // 5 minutes - static final int DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS = 900_000; // 15 minutes - static final int MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS = 300_000; // 5 minutes private final Map mobileKeys; @@ -63,43 +71,46 @@ public class LDConfig { private final Uri eventsUri; private final Uri streamUri; - private final int eventsCapacity; - private final int eventsFlushIntervalMillis; - private final int connectionTimeoutMillis; - private final int pollingIntervalMillis; - private final int backgroundPollingIntervalMillis; - private final int diagnosticRecordingIntervalMillis; - private final int maxCachedUsers; + final ComponentConfigurer dataSource; + final ComponentConfigurer events; + final ComponentConfigurer http; + final ServiceEndpoints serviceEndpoints; - private final boolean stream; - private final boolean offline; - private final boolean disableBackgroundUpdating; - private final boolean useReport; + private final boolean autoAliasingOptOut; private final boolean diagnosticOptOut; + private final boolean disableBackgroundUpdating; + private final boolean evaluationReasons; + private final LDLogAdapter logAdapter; + private final String loggerName; + private final int maxCachedUsers; + private final boolean offline; - private final boolean allAttributesPrivate; - private final Set privateAttributes; - - private final Gson filteredEventGson; + final Gson filteredEventGson; + // deprecated properties that are now in sub-configuration builders + private final boolean allAttributesPrivate; + private final int backgroundPollingIntervalMillis; + private final int connectionTimeoutMillis; + private final int diagnosticRecordingIntervalMillis; + private final int eventsCapacity; + private final int eventsFlushIntervalMillis; + private final LDHeaderUpdater headerTransform; private final boolean inlineUsersInEvents; - - private final boolean evaluationReasons; - + private final int pollingIntervalMillis; + private final Set privateAttributes; + private final boolean stream; + private final boolean useReport; private final String wrapperName; private final String wrapperVersion; - private final LDHeaderUpdater headerTransform; - - private final boolean autoAliasingOptOut; - - private final LDLogAdapter logAdapter; - private final String loggerName; - LDConfig(Map mobileKeys, Uri pollUri, Uri eventsUri, Uri streamUri, + ComponentConfigurer dataSource, + ComponentConfigurer events, + ComponentConfigurer http, + ServiceEndpoints serviceEndpoints, int eventsCapacity, int eventsFlushIntervalMillis, int connectionTimeoutMillis, @@ -127,6 +138,10 @@ public class LDConfig { this.pollUri = pollUri; this.eventsUri = eventsUri; this.streamUri = streamUri; + this.dataSource = dataSource; + this.events = events; + this.http = http; + this.serviceEndpoints = serviceEndpoints; this.eventsCapacity = eventsCapacity; this.eventsFlushIntervalMillis = eventsFlushIntervalMillis; this.connectionTimeoutMillis = connectionTimeoutMillis; @@ -141,7 +156,6 @@ public class LDConfig { this.inlineUsersInEvents = inlineUsersInEvents; this.evaluationReasons = evaluationReasons; this.diagnosticOptOut = diagnosticOptOut; - this.diagnosticRecordingIntervalMillis = diagnosticRecordingIntervalMillis; this.wrapperName = wrapperName; this.wrapperVersion = wrapperVersion; this.maxCachedUsers = maxCachedUsers; @@ -150,38 +164,35 @@ public class LDConfig { this.logAdapter = logAdapter; this.loggerName = loggerName; - this.filteredEventGson = new GsonBuilder() - .registerTypeAdapter(LDUser.class, new LDUtil.LDUserPrivateAttributesTypeAdapter(this)) - .create(); - } - - Headers headersForEnvironment(@NonNull String environmentName, - Map additionalHeaders) { - String sdkKey = mobileKeys.get(environmentName); - - HashMap baseHeaders = new HashMap<>(); - baseHeaders.put("User-Agent", USER_AGENT_HEADER_VALUE); - if (sdkKey != null) { - baseHeaders.put("Authorization", LDConfig.AUTH_SCHEME + sdkKey); - } - - if (getWrapperName() != null) { - String wrapperVersion = ""; - if (getWrapperVersion() != null) { - wrapperVersion = "/" + getWrapperVersion(); + // The following temporary hack is for overriding several deprecated event-related setters + // with the corresponding EventProcessorBuilder setters, if those were used. The problem is + // that in the current SDK implementation, EventProcessor does not actually own the behavior + // that those options are configuring (private attributes, and the diagnostic recording + // interval), so we have to extract those values separately out of the config builder. + boolean actualAllAttributesPrivate = allAttributesPrivate; + Set actualPrivateAttributes = privateAttributes; + int actualDiagnosticRecordingIntervalMillis = diagnosticRecordingIntervalMillis; + if (events instanceof ComponentsImpl.EventProcessorBuilderImpl) { + ComponentsImpl.EventProcessorBuilderImpl eventsBuilder = + (ComponentsImpl.EventProcessorBuilderImpl)events; + actualAllAttributesPrivate = eventsBuilder.isAllAttributesPrivate(); + actualDiagnosticRecordingIntervalMillis = eventsBuilder.getDiagnosticRecordingIntervalMillis(); + actualPrivateAttributes = new HashSet<>(); + if (eventsBuilder.getPrivateAttributes() != null) { + for (String a: eventsBuilder.getPrivateAttributes()) { + actualPrivateAttributes.add(UserAttribute.forName(a)); + } } - baseHeaders.put("X-LaunchDarkly-Wrapper", wrapperName + wrapperVersion); - } - - if (additionalHeaders != null) { - baseHeaders.putAll(additionalHeaders); - } - - if (headerTransform != null) { - headerTransform.updateHeaders(baseHeaders); } + this.diagnosticRecordingIntervalMillis = actualDiagnosticRecordingIntervalMillis; - return Headers.of(baseHeaders); + this.filteredEventGson = new GsonBuilder() + .registerTypeAdapter(LDUser.class, + new LDUtil.LDUserPrivateAttributesTypeAdapter( + actualAllAttributesPrivate, + actualPrivateAttributes + )) + .create(); } public String getMobileKey() { @@ -193,30 +204,85 @@ public Map getMobileKeys() { } /** - * Get the currently configured base URI for polling requests. - * - * @return the base URI configured to be used for poll requests. + * Returns the setting of {@link Builder#pollUri(Uri)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * @return the property value + * @deprecated This method will be removed in the future when individual base URI properties + * are removed from the top-level configuration. */ + @Deprecated public Uri getPollUri() { return pollUri; } + /** + * Returns the setting of {@link Builder#eventsUri(Uri)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * @return the property value + * @deprecated This method will be removed in the future when individual base URI properties + * are removed from the top-level configuration. + */ + @Deprecated public Uri getEventsUri() { return eventsUri; } + /** + * Returns the setting of {@link Builder#eventsCapacity(int)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#events(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual event-related properties + * are removed from the top-level configuration. + */ + @Deprecated public int getEventsCapacity() { return eventsCapacity; } + /** + * Returns the setting of {@link Builder#eventsFlushIntervalMillis(int)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#events(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual event-related properties + * are removed from the top-level configuration. + */ + @Deprecated public int getEventsFlushIntervalMillis() { return eventsFlushIntervalMillis; } + /** + * Returns the setting of {@link Builder#connectionTimeoutMillis(int)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#http(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual HTTP-related properties + * are removed from the top-level configuration. + */ + @Deprecated public int getConnectionTimeoutMillis() { return connectionTimeoutMillis; } + /** + * Returns the setting of {@link Builder#streamUri(Uri)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * @return the property value + * @deprecated This method will be removed in the future when individual base URI properties + * are removed from the top-level configuration. + */ + @Deprecated public Uri getStreamUri() { return streamUri; } @@ -225,18 +291,58 @@ public boolean isOffline() { return offline; } + /** + * Returns the setting of {@link Builder#stream(boolean)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#dataSource(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual data source properties + * are removed from the top-level configuration. + */ + @Deprecated public boolean isStream() { return stream; } + /** + * Returns the setting of {@link Builder#useReport(boolean)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#http(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual HTTP-related properties + * are removed from the top-level configuration. + */ + @Deprecated public boolean isUseReport() { return useReport; } + /** + * Returns the setting of {@link Builder#pollingIntervalMillis(int)} ()}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#dataSource(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual data source properties + * are removed from the top-level configuration. + */ + @Deprecated public int getPollingIntervalMillis() { return pollingIntervalMillis; } + /** + * Returns the setting of {@link Builder#backgroundPollingIntervalMillis(int)} ()}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#dataSource(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual data source properties + * are removed from the top-level configuration. + */ + @Deprecated public int getBackgroundPollingIntervalMillis() { return backgroundPollingIntervalMillis; } @@ -245,18 +351,56 @@ public boolean isDisableBackgroundPolling() { return disableBackgroundUpdating; } + /** + * Returns the setting of {@link Builder#allAttributesPrivate()}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#events(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual event-related properties + * are removed from the top-level configuration. + */ + @Deprecated public boolean allAttributesPrivate() { return allAttributesPrivate; } + /** + * Returns the setting of {@link Builder#privateAttributes(UserAttribute...)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#events(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual event-related properties + * are removed from the top-level configuration. + */ + @Deprecated public Set getPrivateAttributes() { return Collections.unmodifiableSet(privateAttributes); } + /** + * Returns a Gson instance that is configured to serialize event data. This is used internally + * by the SDK; applications should not need to reference it. + * + * @return the Gson instance + * @deprecated Direct access to this object is deprecated and will be removed in the future. + */ + @Deprecated public Gson getFilteredEventGson() { return filteredEventGson; } + /** + * Returns the setting of {@link Builder#inlineUsersInEvents(boolean)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#events(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual event-related properties + * are removed from the top-level configuration. + */ + @Deprecated public boolean inlineUsersInEvents() { return inlineUsersInEvents; } @@ -285,6 +429,16 @@ int getMaxCachedUsers() { return maxCachedUsers; } + /** + * Returns the setting of {@link Builder#headerTransform(LDHeaderUpdater)}. + *

+ * This is only applicable if you have used the deprecated builder method rather than + * {@link Builder#http(ComponentConfigurer)}. + * @return the property value + * @deprecated This method will be removed in the future when individual HTTP-related properties + * are removed from the top-level configuration. + */ + @Deprecated public LDHeaderUpdater getHeaderTransform() { return headerTransform; } @@ -315,12 +469,18 @@ public static class Builder { private Uri eventsUri = DEFAULT_EVENTS_URI; private Uri streamUri = DEFAULT_STREAM_URI; - private int eventsCapacity = DEFAULT_EVENTS_CAPACITY; + private ComponentConfigurer dataSource = null; + private ComponentConfigurer events = null; + private ComponentConfigurer http = null; + private ServiceEndpointsBuilder serviceEndpointsBuilder = null; + + private int eventsCapacity = EventProcessorBuilder.DEFAULT_CAPACITY; private int eventsFlushIntervalMillis = 0; - private int connectionTimeoutMillis = DEFAULT_CONNECTION_TIMEOUT_MILLIS; - private int pollingIntervalMillis = DEFAULT_POLLING_INTERVAL_MILLIS; - private int backgroundPollingIntervalMillis = DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS; - private int diagnosticRecordingIntervalMillis = DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; + private int connectionTimeoutMillis = HttpConfigurationBuilder.DEFAULT_CONNECT_TIMEOUT_MILLIS; + private int pollingIntervalMillis = PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS; + private int backgroundPollingIntervalMillis = DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS; + private int diagnosticRecordingIntervalMillis = + EventProcessorBuilder.DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; private int maxCachedUsers = DEFAULT_MAX_CACHED_USERS; private boolean offline = false; @@ -345,27 +505,37 @@ public static class Builder { private LDLogLevel logLevel = null; /** - * Specifies that user attributes (other than the key) should be hidden from LaunchDarkly. - * If this is set, all user attribute values will be private, not just the attributes - * specified in {@link #privateAttributes(UserAttribute...)}. - * + * Deprecated method for specifying that all user attributes other than the key should be + * hidden from LaunchDarkly. + *

+ * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. + *

* @return the builder + * @deprecated Use {@link #events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#allAttributesPrivate(boolean)} instead. */ + @Deprecated public Builder allAttributesPrivate() { this.allAttributesPrivate = true; return this; } /** - * Marks a set of attributes private. Any users sent to LaunchDarkly with this configuration - * active will have attributes with these names removed. - * + * Deprecated method for marking a set of attributes as private. + *

+ * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. + *

* This can also be specified on a per-user basis with {@link LDUser.Builder} methods like * {@link LDUser.Builder#privateName(String)}. * * @param privateAttributes a set of names that will be removed from user data sent to LaunchDarkly * @return the builder + * @deprecated Use {@link Builder#events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#privateAttributes(String...)} instead. */ + @Deprecated public Builder privateAttributes(UserAttribute... privateAttributes) { this.privateAttributes = new HashSet<>(Arrays.asList(privateAttributes)); return this; @@ -415,132 +585,303 @@ public LDConfig.Builder secondaryMobileKeys(Map secondaryMobileK } /** - * Sets the flag for choosing the REPORT api call. The default is GET. - * Do not use unless advised by LaunchDarkly. + * Deprecated method for specifying whether to use the HTTP REPORT method. + *

+ * The preferred way to set this option now is with {@link HttpConfigurationBuilder}. Any + * settings there will override this deprecated method. * * @param useReport true if HTTP requests should use the REPORT verb * @return the builder + * @deprecated Use {@link Builder#http(ComponentConfigurer)} and + * {@link HttpConfigurationBuilder#useReport(boolean)} instead. */ + @Deprecated public LDConfig.Builder useReport(boolean useReport) { this.useReport = useReport; return this; } /** - * Set the base URI for polling requests to LaunchDarkly. You probably don't need to set this unless instructed by LaunchDarkly. + * Deprecated method for setting the base URI of the polling service. + *

+ * The preferred way to set this option now is with {@link ServiceEndpointsBuilder}. Any + * settings there will override this deprecated method. * - * @param pollUri the URI of the main LaunchDarkly service + * @param pollUri the URI of the LaunchDarkly polling service * @return the builder + * @deprecated Use {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)} and + * {@link ServiceEndpointsBuilder#polling(URI)} instead. */ + @Deprecated public LDConfig.Builder pollUri(Uri pollUri) { this.pollUri = pollUri; return this; } /** - * Set the events URI for sending analytics to LaunchDarkly. You probably don't need to set this unless instructed by LaunchDarkly. + * Deprecated method for setting the base URI of the events service. + *

+ * The preferred way to set this option now is with {@link ServiceEndpointsBuilder}. Any + * settings there will override this deprecated method. * - * @param eventsUri the URI of the LaunchDarkly analytics event service + * @param eventsUri the URI of the LaunchDarkly events service * @return the builder + * @deprecated Use {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)} and + * {@link ServiceEndpointsBuilder#events(URI)} instead. */ + @Deprecated public LDConfig.Builder eventsUri(Uri eventsUri) { this.eventsUri = eventsUri; return this; } /** - * Set the stream URI for connecting to the flag update stream. You probably don't need to set this unless instructed by LaunchDarkly. + * Deprecated method for setting the base URI of the streaming service. + *

+ * The preferred way to set this option now is with {@link ServiceEndpointsBuilder}. Any + * settings there will override this deprecated method. * * @param streamUri the URI of the LaunchDarkly streaming service * @return the builder + * @deprecated Use {@link Builder#serviceEndpoints(ServiceEndpointsBuilder)} and + * {@link ServiceEndpointsBuilder#streaming(URI)} instead. */ + @Deprecated public LDConfig.Builder streamUri(Uri streamUri) { this.streamUri = streamUri; return this; } /** - * Set the capacity of the event buffer. The client buffers up to this many events in memory before flushing. - * If the capacity is exceeded before the buffer is flushed, events will be discarded. Increasing the capacity - * means that events are less likely to be discarded, at the cost of consuming more memory. + * Sets the configuration of the component that receives feature flag data from LaunchDarkly. + *

+ * The default is {@link Components#streamingDataSource()}; you may instead use + * {@link Components#pollingDataSource()}. See those methods for details on how to configure + * them with options that are specific to streaming or polling mode. + *

+ * Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting + * and completely disable network requests. + *


+         *     // Setting custom options when using streaming mode
+         *     LDConfig config = new LDConfig.Builder()
+         *         .dataSource(
+         *             Components.streamingDataSource()
+         *                 .initialReconnectDelayMillis(100)
+         *         )
+         *         .build();
+         *
+         *     // Using polling mode instead of streaming, and setting custom options for polling
+         *     LDConfig config = new LDConfig.Builder()
+         *         .dataSource(
+         *             Components.pollingDataSource()
+         *                 .pollingIntervalMillis(60_000)
+         *         )
+         *         .build();
+         * 
+ * + * @param dataSourceConfigurer the data source configuration builder + * @return the main configuration builder + * @see Components#streamingDataSource() + * @see Components#pollingDataSource() + * @since 3.3.0 + */ + public LDConfig.Builder dataSource(ComponentConfigurer dataSourceConfigurer) { + this.dataSource = dataSourceConfigurer; + return this; + } + + /** + * Sets the implementation of {@link EventProcessor} to be used for processing analytics events. + *

+ * The default is {@link Components#sendEvents()} with no custom options. You may instead call + * {@link Components#sendEvents()} and then set custom options for event processing; or, disable + * events with {@link Components#noEvents()}; or, choose to use a custom implementation (for + * instance, a test fixture). + *

+ * Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting + * and completely disable network requests. + *


+         *     // Setting custom event processing options
+         *     LDConfig config = new LDConfig.Builder()
+         *         .events(Components.sendEvents().capacity(100))
+         *         .build();
+         *
+         *     // Disabling events
+         *     LDConfig config = new LDConfig.Builder()
+         *         .events(Components.noEvents())
+         *         .build();
+         * 
+ * + * @param eventsConfigurer the events configuration builder + * @return the main configuration builder + * @since 3.3.0 + * @see Components#sendEvents() + * @see Components#noEvents() + */ + public LDConfig.Builder events(ComponentConfigurer eventsConfigurer) { + this.events = eventsConfigurer; + return this; + } + + /** + * Sets the SDK's networking configuration, using a configuration builder. This builder is + * obtained from {@link Components#httpConfiguration()}, and has methods for setting individual + * HTTP-related properties. + *

+         *     LDConfig config = new LDConfig.Builder()
+         *         .http(Components.httpConfiguration().connectTimeoutMillis(5000))
+         *         .build();
+         * 
+ * + * @param httpConfigurer the HTTP configuration builder + * @return the main configuration builder + * @since 3.3.0 + * @see Components#httpConfiguration() + */ + public Builder http(ComponentConfigurer httpConfigurer) { + this.http = httpConfigurer; + return this; + } + + /** + * Sets the base service URIs used by SDK components. + *

+ * This object is a configuration builder obtained from {@link Components#serviceEndpoints()}, + * which has methods for setting each external endpoint to a custom URI. + *


+         *     LDConfig config = new LDConfig.Builder().mobileKey("key")
+         *         .serviceEndpoints(
+         *             Components.serviceEndpoints().relayProxy("http://my-relay-proxy-host")
+         *         );
+         * 
+ * + * @param serviceEndpointsBuilder a configuration builder object returned by {@link Components#serviceEndpoints()} + * @return the builder + * @since 3.3.0 + */ + public Builder serviceEndpoints(ServiceEndpointsBuilder serviceEndpointsBuilder) { + this.serviceEndpointsBuilder = serviceEndpointsBuilder; + return this; + } + + /** + * Deprecated method for setting the capacity of the event buffer. *

- * The default value is {@link #DEFAULT_EVENTS_CAPACITY}. + * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. + *

+ * The default value is {@link EventProcessorBuilder#DEFAULT_CAPACITY}. * * @param eventsCapacity the capacity of the event buffer * @return the builder * @see #eventsFlushIntervalMillis(int) + * @deprecated Use {@link Builder#events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#capacity(int)} instead. */ + @Deprecated public LDConfig.Builder eventsCapacity(int eventsCapacity) { this.eventsCapacity = eventsCapacity; return this; } /** - * Sets the maximum amount of time to wait in between sending analytics events to LaunchDarkly. + * Deprecated method for setting the maximum amount of time to wait in between sending + * analytics events to LaunchDarkly. + *

+ * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. *

- * The default value is {@link #DEFAULT_FLUSH_INTERVAL_MILLIS}. + * The default value is {@link EventProcessorBuilder#DEFAULT_FLUSH_INTERVAL_MILLIS}. * * @param eventsFlushIntervalMillis the interval between event flushes, in milliseconds * @return the builder * @see #eventsCapacity(int) + * @deprecated Use {@link Builder#events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#flushIntervalMillis(int)} instead. */ + @Deprecated public LDConfig.Builder eventsFlushIntervalMillis(int eventsFlushIntervalMillis) { this.eventsFlushIntervalMillis = eventsFlushIntervalMillis; return this; } - /** - * Sets the timeout when connecting to LaunchDarkly. + * Deprecated method for setting the connection timeout. *

- * The default value is {@link #DEFAULT_CONNECTION_TIMEOUT_MILLIS}. + * The preferred way to set this option now is with {@link HttpConfigurationBuilder}. Any + * settings there will override this deprecated method. * * @param connectionTimeoutMillis the connection timeout, in milliseconds * @return the builder + * @deprecated Use {@link Builder#http(ComponentConfigurer)} and + * {@link HttpConfigurationBuilder#connectTimeoutMillis(int)} instead. */ + @Deprecated public LDConfig.Builder connectionTimeoutMillis(int connectionTimeoutMillis) { this.connectionTimeoutMillis = connectionTimeoutMillis; return this; } - /** - * Enables or disables real-time streaming flag updates. By default, streaming is enabled. - * When disabled, an efficient caching polling mechanism is used. + * Deprecated method for enabling or disabling real-time streaming flag updates. + *

+ * The preferred way to set this option now is with {@link StreamingDataSourceBuilder}. Any + * settings there will override this deprecated method. Setting this option to {@code false} + * is equivalent to calling {@code builder.dataSource(Components.pollingDataSource())}. + *

+ * By default, streaming is enabled. * * @param enabled true if streaming should be enabled * @return the builder + * @deprecated Use {@link Builder#dataSource(ComponentConfigurer)} with either + * {@link Components#streamingDataSource()} or {@link Components#pollingDataSource()} + * instead. */ + @Deprecated public LDConfig.Builder stream(boolean enabled) { this.stream = enabled; return this; } /** - * Sets the interval in between feature flag updates, when streaming mode is disabled. - * This is ignored unless {@link #stream(boolean)} is set to {@code true}. When set, it - * will also change the default value for {@link #eventsFlushIntervalMillis(int)} to the - * same value. + * Deprecated method for setting the interval in between feature flag updates, when + * streaming mode is disabled. *

- * The default value is {@link LDConfig#DEFAULT_POLLING_INTERVAL_MILLIS}. + * The preferred way to set this option now is with {@link PollingDataSourceBuilder}. Any + * settings there will override this deprecated method. + *

+ * The default value is {@link PollingDataSourceBuilder#DEFAULT_POLL_INTERVAL_MILLIS}. * * @param pollingIntervalMillis the feature flag polling interval, in milliseconds * @return the builder + * @deprecated Use {@link Builder#dataSource(ComponentConfigurer)} and + * {@link PollingDataSourceBuilder#pollIntervalMillis(int)} instead. */ + @Deprecated public LDConfig.Builder pollingIntervalMillis(int pollingIntervalMillis) { this.pollingIntervalMillis = pollingIntervalMillis; return this; } /** - * Sets how often the client will poll for flag updates when your application is in the background. + * Deprecated method for setting how often the client will poll for flag updates when your + * application is in the background. + *

+ * The preferred way to set this option now is with {@link StreamingDataSourceBuilder} or + * {@link PollingDataSourceBuilder} (depending on whether you want the SDK to use streaming + * or polling when it is in the foreground). Any settings there will override this + * deprecated method. *

- * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS}. + * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS}. * * @param backgroundPollingIntervalMillis the feature flag polling interval when in the background, * in milliseconds * @return the builder + * @deprecated Use {@link Builder#dataSource(ComponentConfigurer)} and either + * {@link StreamingDataSourceBuilder#backgroundPollIntervalMillis(int)} or + * {@link PollingDataSourceBuilder#backgroundPollIntervalMillis(int)} instead. */ + @Deprecated public LDConfig.Builder backgroundPollingIntervalMillis(int backgroundPollingIntervalMillis) { this.backgroundPollingIntervalMillis = backgroundPollingIntervalMillis; return this; @@ -576,15 +917,20 @@ public LDConfig.Builder offline(boolean offline) { } /** - * If enabled, events to the server will be created containing the entire User object. - * If disabled, events to the server will be created without the entire User object, including only the user key instead; - * the rest of the user properties will still be included in Identify events. + * Deprecated method for specifying whether events sent to the server will always include + * the full user object. + *

+ * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. *

- * Defaults to false in order to reduce network bandwidth. + * This defaults to false in order to reduce network bandwidth. * * @param inlineUsersInEvents true if all user properties should be included in events * @return the builder + * @deprecated Use {@link Builder#events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#inlineUsers(boolean)} instead. */ + @Deprecated public LDConfig.Builder inlineUsersInEvents(boolean inlineUsersInEvents) { this.inlineUsersInEvents = inlineUsersInEvents; return this; @@ -624,41 +970,52 @@ public LDConfig.Builder diagnosticOptOut(boolean diagnosticOptOut) { } /** - * Sets the interval at which periodic diagnostic data is sent. The default is every 15 minutes (900,000 - * milliseconds) and the minimum value is 300,000 (5 minutes). - * - * @see #diagnosticOptOut(boolean) for more information on the diagnostics data being sent. + * Deprecatd method for setting the interval at which periodic diagnostic data is sent. + *

+ * The preferred way to set this option now is with {@link EventProcessorBuilder}. Any + * settings there will override this deprecated method. * * @param diagnosticRecordingIntervalMillis the diagnostics interval in milliseconds * @return the builder + * @deprecated Use {@link Builder#events(ComponentConfigurer)} and + * {@link EventProcessorBuilder#diagnosticRecordingIntervalMillis(int)} instead. + * @see #diagnosticOptOut(boolean) */ + @Deprecated public LDConfig.Builder diagnosticRecordingIntervalMillis(int diagnosticRecordingIntervalMillis) { this.diagnosticRecordingIntervalMillis = diagnosticRecordingIntervalMillis; return this; } /** - * For use by wrapper libraries to set an identifying name for the wrapper being used. This will be sent in - * User-Agent headers during requests to the LaunchDarkly servers to allow recording metrics on the usage of - * these wrapper libraries. + * Deprecated method for setting a wrapper library name to include in User-Agent headers. + *

+ * The preferred way to set this option now is with {@link HttpConfigurationBuilder}. Any + * settings there will override this deprecated method. * - * @param wrapperName An identifying name for the wrapper library + * @param wrapperName an identifying name for the wrapper library * @return the builder + * @deprecated Use {@link Builder#http(ComponentConfigurer)} and + * {@link HttpConfigurationBuilder#wrapper(String, String)} instead. */ + @Deprecated public LDConfig.Builder wrapperName(String wrapperName) { this.wrapperName = wrapperName; return this; } /** - * For use by wrapper libraries to report the version of the library in use. If the wrapper - * name has not been set with {@link #wrapperName(String)} this field will be ignored. - * Otherwise the version string will be included in the User-Agent headers along with the - * wrapperName during requests to the LaunchDarkly servers. + * Deprecated method for setting a wrapper library version to include in User-Agent headers. + *

+ * The preferred way to set this option now is with {@link HttpConfigurationBuilder}. Any + * settings there will override this deprecated method. * - * @param wrapperVersion Version string for the wrapper library + * @param wrapperVersion a version string for the wrapper library * @return the builder + * @deprecated Use {@link Builder#http(ComponentConfigurer)} and + * {@link HttpConfigurationBuilder#wrapper(String, String)} instead. */ + @Deprecated public LDConfig.Builder wrapperVersion(String wrapperVersion) { this.wrapperVersion = wrapperVersion; return this; @@ -693,11 +1050,17 @@ public LDConfig.Builder autoAliasingOptOut(boolean autoAliasingOptOut) { } /** - * Provides a callback for dynamically modifying headers used on requests to the LaunchDarkly service. + * Deprecated method for dynamically modifying request headers. + *

+ * The preferred way to set this option now is with {@link HttpConfigurationBuilder}. Any + * settings there will override this deprecated method. * * @param headerTransform the transformation to apply to requests * @return the builder + * @deprecated Use {@link Builder#http(ComponentConfigurer)} and + * {@link HttpConfigurationBuilder#headerTransform(LDHeaderUpdater)} instead. */ + @Deprecated public LDConfig.Builder headerTransform(LDHeaderUpdater headerTransform) { this.headerTransform = headerTransform; return this; @@ -812,61 +1175,114 @@ public LDConfig build() { LDLogger logger = LDLogger.withAdapter(actualLogAdapter, loggerName); - if (!stream) { - if (pollingIntervalMillis < MIN_POLLING_INTERVAL_MILLIS) { - logger.warn( - "setPollingIntervalMillis: {} was set below the allowed minimum of: {}. Ignoring and using minimum value.", - pollingIntervalMillis, MIN_POLLING_INTERVAL_MILLIS); - pollingIntervalMillis = MIN_POLLING_INTERVAL_MILLIS; - } + if (diagnosticRecordingIntervalMillis < EventProcessorBuilder.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS) { + logger.warn( + "diagnosticRecordingIntervalMillis was set to %s, lower than the minimum allowed (%s). Ignoring and using minimum value.", + diagnosticRecordingIntervalMillis, EventProcessorBuilder.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS); + diagnosticRecordingIntervalMillis = EventProcessorBuilder.MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; + } - if (!disableBackgroundUpdating && backgroundPollingIntervalMillis < pollingIntervalMillis) { - logger.warn( - "BackgroundPollingIntervalMillis: {} was set below the foreground polling interval: {}. Ignoring and using minimum value for background polling.", - backgroundPollingIntervalMillis, pollingIntervalMillis); - backgroundPollingIntervalMillis = MIN_BACKGROUND_POLLING_INTERVAL_MILLIS; - } + HashMap mobileKeys; + if (secondaryMobileKeys == null) { + mobileKeys = new HashMap<>(); + } + else { + mobileKeys = new HashMap<>(secondaryMobileKeys); + } + mobileKeys.put(primaryEnvironmentName, mobileKey); - if (eventsFlushIntervalMillis == 0) { - eventsFlushIntervalMillis = pollingIntervalMillis; - // this is a normal occurrence, so don't log a warning about it + ComponentConfigurer dataSourceConfig = this.dataSource; + if (dataSourceConfig == null) { + // Copy the deprecated properties to the new data source configuration builder. + // There is some additional validation logic here that is specific to the + // deprecated property setters; the new configuration builder, in keeping with the + // standard behavior of other configuration builders in the Java and Android SDKs, + // doesn't log such messages. + + if (!disableBackgroundUpdating) { + if (backgroundPollingIntervalMillis < MIN_BACKGROUND_POLL_INTERVAL_MILLIS) { + logger.warn( + "BackgroundPollingIntervalMillis: {} was set below the minimum allowed: {}. Ignoring and using minimum value.", + backgroundPollingIntervalMillis, MIN_BACKGROUND_POLL_INTERVAL_MILLIS); + backgroundPollingIntervalMillis = MIN_BACKGROUND_POLL_INTERVAL_MILLIS; + } } - } - if (!disableBackgroundUpdating) { - if (backgroundPollingIntervalMillis < MIN_BACKGROUND_POLLING_INTERVAL_MILLIS) { - logger.warn( - "BackgroundPollingIntervalMillis: {} was set below the minimum allowed: {}. Ignoring and using minimum value.", - backgroundPollingIntervalMillis, MIN_BACKGROUND_POLLING_INTERVAL_MILLIS); - backgroundPollingIntervalMillis = MIN_BACKGROUND_POLLING_INTERVAL_MILLIS; + if (stream) { + dataSourceConfig = Components.streamingDataSource() + .backgroundPollIntervalMillis(backgroundPollingIntervalMillis); + } else { + if (pollingIntervalMillis < PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS) { + // the default is also the minimum + logger.warn( + "setPollingIntervalMillis: {} was set below the allowed minimum of: {}. Ignoring and using minimum value.", + pollingIntervalMillis, PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS); + pollingIntervalMillis = PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS; + } + + if (!disableBackgroundUpdating && backgroundPollingIntervalMillis < pollingIntervalMillis) { + logger.warn( + "BackgroundPollingIntervalMillis: {} was set below the foreground polling interval: {}. Ignoring and using minimum value for background polling.", + backgroundPollingIntervalMillis, pollingIntervalMillis); + backgroundPollingIntervalMillis = MIN_BACKGROUND_POLL_INTERVAL_MILLIS; + } + + if (eventsFlushIntervalMillis == 0) { + // This behavior is retained for historical reasons; the newer configuration + // builder does not modify properties like this that are outside its scope. + eventsFlushIntervalMillis = pollingIntervalMillis; + } + + dataSourceConfig = Components.pollingDataSource() + .backgroundPollIntervalMillis(backgroundPollingIntervalMillis) + .pollIntervalMillis(pollingIntervalMillis); } } if (eventsFlushIntervalMillis == 0) { - eventsFlushIntervalMillis = DEFAULT_FLUSH_INTERVAL_MILLIS; // this is a normal occurrence, so don't log a warning about it + eventsFlushIntervalMillis = EventProcessorBuilder.DEFAULT_FLUSH_INTERVAL_MILLIS; + // this is a normal occurrence, so don't log a warning about it } - if (diagnosticRecordingIntervalMillis < MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS) { - logger.warn( - "diagnosticRecordingIntervalMillis was set to %s, lower than the minimum allowed (%s). Ignoring and using minimum value.", - diagnosticRecordingIntervalMillis, MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS); - diagnosticRecordingIntervalMillis = MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; + ComponentConfigurer eventsConfig = this.events; + if (eventsConfig == null) { + // Copy the deprecated properties to the new events configuration builder. + EventProcessorBuilder eventsBuilder = Components.sendEvents() + .allAttributesPrivate(allAttributesPrivate) + .capacity(eventsCapacity) + .diagnosticRecordingIntervalMillis(diagnosticRecordingIntervalMillis) + .flushIntervalMillis(eventsFlushIntervalMillis) + .inlineUsers(inlineUsersInEvents); + if (privateAttributes != null) { + eventsBuilder.privateAttributes(privateAttributes.toArray(new UserAttribute[privateAttributes.size()])); + } + eventsConfig = eventsBuilder; } - HashMap mobileKeys; - if (secondaryMobileKeys == null) { - mobileKeys = new HashMap<>(); - } - else { - mobileKeys = new HashMap<>(secondaryMobileKeys); + ComponentConfigurer httpConfig = this.http; + if (httpConfig == null) { + // Copy the deprecated properties to the new HTTP configuration builder. + HttpConfigurationBuilder httpBuilder = Components.httpConfiguration() + .connectTimeoutMillis(connectionTimeoutMillis) + .headerTransform(headerTransform) + .useReport(useReport) + .wrapper(wrapperName, wrapperVersion); + httpConfig = httpBuilder; } - mobileKeys.put(primaryEnvironmentName, mobileKey); + + ServiceEndpoints serviceEndpoints = this.serviceEndpointsBuilder == null ? + Components.serviceEndpoints().polling(pollUri).streaming(streamUri).events(eventsUri).build() : + this.serviceEndpointsBuilder.build(); return new LDConfig( mobileKeys, pollUri, eventsUri, streamUri, + dataSourceConfig, + eventsConfig, + httpConfig, + serviceEndpoints, eventsCapacity, eventsFlushIntervalMillis, connectionTimeoutMillis, 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 c3ef2926..12520d92 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 @@ -19,6 +19,7 @@ import com.launchdarkly.sdk.LDUser; import com.launchdarkly.sdk.LDValue; import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; import java.io.IOException; import java.util.Arrays; @@ -27,7 +28,31 @@ import java.util.Map; import java.util.Set; +import okhttp3.Headers; + class LDUtil { + static final String AUTH_SCHEME = "api_key "; + static final String USER_AGENT_HEADER_VALUE = "AndroidClient/" + BuildConfig.VERSION_NAME; + + static Headers makeRequestHeaders( + @NonNull HttpConfiguration httpConfig, + Map additionalHeaders + ) { + HashMap baseHeaders = new HashMap<>(); + for (Map.Entry kv: httpConfig.getDefaultHeaders()) { + baseHeaders.put(kv.getKey(), kv.getValue()); + } + + if (additionalHeaders != null) { + baseHeaders.putAll(additionalHeaders); + } + + if (httpConfig.getHeaderTransform() != null) { + httpConfig.getHeaderTransform().updateHeaders(baseHeaders); + } + + return Headers.of(baseHeaders); + } /** * Looks at the Android device status to determine if the device is online. @@ -157,15 +182,20 @@ private static void logException(LDLogger logger, Throwable ex, boolean asError, } static class LDUserPrivateAttributesTypeAdapter extends TypeAdapter { - private final LDConfig config; - - LDUserPrivateAttributesTypeAdapter(LDConfig cfg) { - config = cfg; + private final boolean allAttributesPrivate; + private final Set privateAttributes; + + LDUserPrivateAttributesTypeAdapter( + boolean allAttributesPrivate, + Set privateAttributes + ) { + this.allAttributesPrivate = allAttributesPrivate; + this.privateAttributes = privateAttributes; } private boolean isPrivate(LDUser user, UserAttribute attribute) { - return config.allAttributesPrivate() || - config.getPrivateAttributes().contains(attribute) || + return allAttributesPrivate || + privateAttributes.contains(attribute) || user.isAttributePrivate(attribute); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java index 1fe17d4f..c6f5c363 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/PollingUpdater.java @@ -21,7 +21,7 @@ */ public class PollingUpdater extends BroadcastReceiver { - private static int backgroundPollingIntervalMillis = LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS; + private static int backgroundPollingIntervalMillis = LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS; private static AtomicBoolean pollingActive = new AtomicBoolean(false); private static AtomicInteger pollingInterval = new AtomicInteger(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java new file mode 100644 index 00000000..c9c395e3 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/StandardEndpoints.java @@ -0,0 +1,55 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.logging.LDLogger; + +import java.net.URI; + +abstract class StandardEndpoints { + private StandardEndpoints() {} + + static final URI DEFAULT_STREAMING_BASE_URI = URI.create("https://clientstream.launchdarkly.com"); + static final URI DEFAULT_POLLING_BASE_URI = URI.create("https://clientsdk.launchdarkly.com"); + static final URI DEFAULT_EVENTS_BASE_URI = URI.create("https://mobile.launchdarkly.com"); + + static final String STREAMING_REQUEST_BASE_PATH = "/meval"; + static final String POLLING_REQUEST_GET_BASE_PATH = "/msdk/evalx/contexts"; + static final String POLLING_REQUEST_REPORT_BASE_PATH = "/msdk/evalx/context"; + static final String ANALYTICS_EVENTS_REQUEST_PATH = "/mobile/events/bulk"; + static final String DIAGNOSTIC_EVENTS_REQUEST_PATH = "/mobile/events/diagnostic"; + + /** + * Internal method to decide which URI a given component should connect to. + *

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

+ * This boolean value is only used for our diagnostic events. We only check if the value + * differs from the default; if the base URI was "overridden" in configuration, but + * happens to be equal to the default URI, we don't count that as custom + * for the purposes of this diagnostic. + * + * @param serviceEndpointsValue the value set in ServiceEndpoints + * @param defaultValue the constant default URI value defined in StandardEndpoints + * @return true iff the base URI was customized + */ + static boolean isCustomBaseUri(URI serviceEndpointsValue, URI defaultValue) { + return serviceEndpointsValue != null && !serviceEndpointsValue.equals(defaultValue); + } +} 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 238023e2..4bf367a5 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 @@ -8,8 +8,10 @@ import com.launchdarkly.eventsource.MessageEvent; import com.launchdarkly.eventsource.UnsuccessfulResponseException; import com.launchdarkly.logging.LDLogger; -import com.launchdarkly.logging.LogValues; import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.android.subsystems.DataSource; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; +import com.launchdarkly.sdk.android.subsystems.ServiceEndpoints; import java.net.URI; import java.util.HashMap; @@ -35,8 +37,11 @@ class StreamUpdateProcessor { private static final long MAX_RECONNECT_TIME_MS = 3_600_000; // 1 hour private EventSource es; + private final DataSource dataSourceConfig; + private final HttpConfiguration httpConfig; private final LDConfig config; private final UserManager userManager; + private final Uri streamUri; private volatile boolean running = false; private final Debounce queue; private boolean connection401Error = false; @@ -47,10 +52,22 @@ class StreamUpdateProcessor { private long eventSourceStarted; private final LDLogger logger; - StreamUpdateProcessor(LDConfig config, UserManager userManager, String environmentName, DiagnosticStore diagnosticStore, - LDUtil.ResultCallback notifier, LDLogger logger) { + StreamUpdateProcessor( + LDConfig config, + DataSource dataSourceConfig, + HttpConfiguration httpConfig, + URI streamUri, + UserManager userManager, + String environmentName, + DiagnosticStore diagnosticStore, + LDUtil.ResultCallback notifier, + LDLogger logger + ) { this.config = config; + this.dataSourceConfig = dataSourceConfig; + this.httpConfig = httpConfig; this.userManager = userManager; + this.streamUri = Uri.parse(streamUri.toString()); this.environmentName = environmentName; this.notifier = notifier; this.diagnosticStore = diagnosticStore; @@ -123,6 +140,8 @@ public void onError(Throwable t) { }; EventSource.Builder builder = new EventSource.Builder(handler, getUri(userManager.getCurrentUser())); + builder.connectTimeoutMs(httpConfig.getConnectTimeoutMillis()); + builder.reconnectTimeMs(dataSourceConfig.getInitialReconnectDelayMillis()); builder.requestTransformer(input -> { Map> esHeaders = input.headers().toMultimap(); @@ -135,10 +154,12 @@ public void onError(Throwable t) { break; } } - return input.newBuilder().headers(config.headersForEnvironment(environmentName, collapsed)).build(); + return input.newBuilder().headers( + LDUtil.makeRequestHeaders(httpConfig, collapsed) + ).build(); }); - if (config.isUseReport()) { + if (httpConfig.isUseReport()) { builder.method(METHOD_REPORT); builder.body(getRequestBody(userManager.getCurrentUser())); } @@ -160,9 +181,9 @@ private RequestBody getRequestBody(@Nullable LDUser user) { } private URI getUri(@Nullable LDUser user) { - String str = Uri.withAppendedPath(config.getStreamUri(), "meval").toString(); + String str = Uri.withAppendedPath(streamUri, "meval").toString(); - if (!config.isUseReport() && user != null) { + if (!httpConfig.isUseReport() && user != null) { str += "/" + DefaultUserManager.base64Url(user); } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java new file mode 100644 index 00000000..28b624bf --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/EventProcessorBuilder.java @@ -0,0 +1,178 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.UserAttribute; +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDConfig.Builder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.EventProcessor; + +import java.util.HashSet; +import java.util.Set; + +/** + * Contains methods for configuring delivery of analytics events. + *

+ * The SDK normally buffers analytics events and sends them to LaunchDarkly at intervals. If you want + * to customize this behavior, create a builder with {@link Components#sendEvents()}, change its + * properties with the methods of this class, and pass it to {@link Builder#events(ComponentConfigurer)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .events(Components.sendEvents().capacity(500).flushIntervalMillis(2000))
+ *         .build();
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#sendEvents()}. + * + * @since 3.3.0 + */ +public abstract class EventProcessorBuilder implements ComponentConfigurer { + /** + * The default value for {@link #capacity(int)}. + */ + public static final int DEFAULT_CAPACITY = 100; + + /** + * The default value for {@link #diagnosticRecordingIntervalMillis(int)}: 15 minutes. + */ + public static final int DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS = 900_000; + + /** + * The default value for {@link #flushIntervalMillis(int)}: 30 seconds. + */ + public static final int DEFAULT_FLUSH_INTERVAL_MILLIS = 30_000; + + /** + * The minimum value for {@link #diagnosticRecordingIntervalMillis(int)}: 5 minutes. + */ + public static final int MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS = 300_000; + + protected boolean allAttributesPrivate = false; + protected int capacity = DEFAULT_CAPACITY; + protected int diagnosticRecordingIntervalMillis = DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS; + protected int flushIntervalMillis = DEFAULT_FLUSH_INTERVAL_MILLIS; + protected boolean inlineUsers = false; + protected Set privateAttributes; + + /** + * Sets whether or not all optional user attributes should be hidden from LaunchDarkly. + *

+ * If this is {@code true}, all user attribute values (other than the key) will be private, not just + * the attributes specified in {@link #privateAttributes(String...)} or on a per-user basis with + * {@link com.launchdarkly.sdk.LDUser.Builder} methods. By default, it is {@code false}. + * + * @param allAttributesPrivate true if all user attributes should be private + * @return the builder + * @see #privateAttributes(String...) + * @see com.launchdarkly.sdk.LDUser.Builder + */ + public EventProcessorBuilder allAttributesPrivate(boolean allAttributesPrivate) { + this.allAttributesPrivate = allAttributesPrivate; + return this; + } + + /** + * Set the capacity of the events buffer. + *

+ * The client buffers up to this many events in memory before flushing. If the capacity is exceeded before + * the buffer is flushed (see {@link #flushIntervalMillis(int)}, events will be discarded. Increasing the + * capacity means that events are less likely to be discarded, at the cost of consuming more memory. + *

+ * The default value is {@link #DEFAULT_CAPACITY}. + * + * @param capacity the capacity of the event buffer + * @return the builder + */ + public EventProcessorBuilder capacity(int capacity) { + this.capacity = capacity; + return this; + } + + /** + * Sets the interval at which periodic diagnostic data is sent. + *

+ * The default value is {@link #DEFAULT_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS}; the minimum value is + * {@link #MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS}. This property is ignored if + * {@link Builder#diagnosticOptOut(boolean)} is set to {@code true}. + * + * @param diagnosticRecordingIntervalMillis the diagnostics interval in milliseconds + * @return the builder + */ + public EventProcessorBuilder diagnosticRecordingIntervalMillis(int diagnosticRecordingIntervalMillis) { + this.diagnosticRecordingIntervalMillis = diagnosticRecordingIntervalMillis < MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS ? + MIN_DIAGNOSTIC_RECORDING_INTERVAL_MILLIS : diagnosticRecordingIntervalMillis; + return this; + } + + /** + * Sets the interval between flushes of the event buffer. + *

+ * Decreasing the flush interval means that the event buffer is less likely to reach capacity. + *

+ * The default value is {@link #DEFAULT_FLUSH_INTERVAL_MILLIS}. + * + * @param flushIntervalMillis the flush interval in milliseconds + * @return the builder + */ + public EventProcessorBuilder flushIntervalMillis(int flushIntervalMillis) { + this.flushIntervalMillis = flushIntervalMillis <= 0 ? DEFAULT_FLUSH_INTERVAL_MILLIS : flushIntervalMillis; + return this; + } + + /** + * If enabled, events to the server will be created containing the entire LDUser object. + * If disabled, events to the server will be created without the entire LDUser object, including + * only the user key instead; the rest of the user properties will still be included in Identify + * events. + *

+ * Defaults to false in order to reduce network bandwidth. + * + * @param inlineUsers true if all user properties should be included in events + * @return the builder + */ + public EventProcessorBuilder inlineUsers(boolean inlineUsers) { + this.inlineUsers = inlineUsers; + return this; + } + + /** + * Marks a set of attribute names or subproperties as private. + *

+ * Any contexts sent to LaunchDarkly with this configuration active will have attributes with these + * names removed. This is in addition to any attributes that were marked as private for an + * individual context with {@link com.launchdarkly.sdk.LDUser.Builder} methods. + *

+ * This method replaces any previous private attributes that were set on the same builder, rather + * than adding to them. + * + * @param attributeNames a set of attribute names that will be removed from context data set to LaunchDarkly + * @return the builder + * @see #allAttributesPrivate(boolean) + * @see com.launchdarkly.sdk.LDUser.Builder + */ + public EventProcessorBuilder privateAttributes(String... attributeNames) { + privateAttributes = new HashSet<>(); + for (String a: attributeNames) { + privateAttributes.add(a); + } + return this; + } + + /** + * Marks a set of attribute names or subproperties as private. + *

+ * This is the same as {@link #privateAttributes(String...)}, but uses the + * {@link UserAttribute} type. + * + * @param attributeNames a set of attribute names that will be removed from context data set to LaunchDarkly + * @return the builder + * @see #allAttributesPrivate(boolean) + * @see com.launchdarkly.sdk.LDUser.Builder + */ + public EventProcessorBuilder privateAttributes(UserAttribute... attributeNames) { + privateAttributes = new HashSet<>(); + for (UserAttribute a: attributeNames) { + privateAttributes.add(a.getName()); + } + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java new file mode 100644 index 00000000..66bf25e1 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/HttpConfigurationBuilder.java @@ -0,0 +1,99 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDHeaderUpdater; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.HttpConfiguration; + +/** + * Contains methods for configuring the SDK's networking behavior. + *

+ * If you want to set non-default values for any of these properties, create a builder with + * {@link Components#httpConfiguration()}, change its properties with the methods of this class, + * and pass it to {@link com.launchdarkly.sdk.android.LDConfig.Builder#http(ComponentConfigurer)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .http(
+ *           Components.httpConfiguration()
+ *             .connectTimeoutMillis(3000)
+ *             .proxyHostAndPort("my-proxy", 8080)
+ *          )
+ *         .build();
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#httpConfiguration()}. + * + * @since 3.3.0 + */ +public abstract class HttpConfigurationBuilder implements ComponentConfigurer { + /** + * The default value for {@link #connectTimeoutMillis(int)}: ten seconds. + */ + public static final int DEFAULT_CONNECT_TIMEOUT_MILLIS = 10000; + + protected int connectTimeoutMillis = DEFAULT_CONNECT_TIMEOUT_MILLIS; + protected LDHeaderUpdater headerTransform; + protected boolean useReport; + protected String wrapperName; + protected String wrapperVersion; + + /** + * Sets the connection timeout. This is the time allowed for the SDK to make a socket connection to + * any of the LaunchDarkly services. + *

+ * The default is {@link #DEFAULT_CONNECT_TIMEOUT_MILLIS}. + * + * @param connectTimeoutMillis the connection timeout in milliseconds + * @return the builder + */ + public HttpConfigurationBuilder connectTimeoutMillis(int connectTimeoutMillis) { + this.connectTimeoutMillis = connectTimeoutMillis <= 0 ? DEFAULT_CONNECT_TIMEOUT_MILLIS : + connectTimeoutMillis; + return this; + } + + /** + * Provides a callback for dynamically modifying headers used on requests to LaunchDarkly services. + * + * @param headerTransform the transformation to apply to requests + * @return the builder + */ + public HttpConfigurationBuilder headerTransform(LDHeaderUpdater headerTransform) { + this.headerTransform = headerTransform; + return this; + } + + /** + * Sets whether to use the HTTP REPORT method for feature flag requests. + *

+ * By default, polling and streaming connections are made with the GET method, with the context + * data encoded into the request URI. Using REPORT allows the user data to be sent in the request + * body instead, which is somewhat more secure and efficient. + *

+ * However, the REPORT method is not always supported by operating systems or network gateways. + * Therefore it is disabled in the SDK by default. You can enable it if you know your code will + * not be running in an environment that disallows REPORT. + * + * @param useReport true to enable the REPORT method + * @return the builder + */ + public HttpConfigurationBuilder useReport(boolean useReport) { + this.useReport = useReport; + return this; + } + + /** + * For use by wrapper libraries to set an identifying name for the wrapper being used. This will be included in a + * header during requests to the LaunchDarkly servers to allow recording metrics on the usage of + * these wrapper libraries. + * + * @param wrapperName an identifying name for the wrapper library + * @param wrapperVersion version string for the wrapper library + * @return the builder + */ + public HttpConfigurationBuilder wrapper(String wrapperName, String wrapperVersion) { + this.wrapperName = wrapperName; + this.wrapperVersion = wrapperVersion; + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java new file mode 100644 index 00000000..f8cf01ee --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingDataSourceBuilder.java @@ -0,0 +1,77 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDConfig.Builder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DataSource; + +/** + * Contains methods for configuring the polling data source. + *

+ * Polling is not the default behavior; by default, the SDK uses a streaming connection to receive + * feature flag data from LaunchDarkly whenever the application is in the foreground. In polling + * mode, the SDK instead makes a new HTTP request to LaunchDarkly at regular intervals. HTTP + * caching allows it to avoid redundantly downloading data if there have been no changes, but + * polling is still less efficient than streaming and should only be used on the advice of + * LaunchDarkly support. + *

+ * To use polling mode, create a builder with {@link Components#pollingDataSource()}, set any custom + * options if desired with the methods of this class, and pass it to + * {@link Builder#dataSource(ComponentConfigurer)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataSource(Components.pollingDataSource().pollIntervalMillis(30000))
+ *         .build();
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling + * {@link Components#pollingDataSource()}. + * + * @since 3.3.0 + */ +public abstract class PollingDataSourceBuilder implements ComponentConfigurer { + /** + * The default value for {@link #pollIntervalMillis(int)}: 5 minutes. + */ + public static final int DEFAULT_POLL_INTERVAL_MILLIS = 300_000; + + protected int backgroundPollIntervalMillis = LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS; + protected int pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS; + + /** + * Sets the interval between feature flag updates when the application is running in the background. + *

+ * This is normally a longer interval than the foreground polling interval ({@link #pollIntervalMillis(int)}). + * It is ignored if you have turned off background polling entirely by setting + * {@link Builder#disableBackgroundUpdating(boolean)}. + *

+ * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS}; the minimum + * is {@link LDConfig#MIN_BACKGROUND_POLL_INTERVAL_MILLIS}. + * + * @param backgroundPollIntervalMillis the background polling interval in milliseconds + * @return the builder + * @see #pollIntervalMillis(int) + */ + public PollingDataSourceBuilder backgroundPollIntervalMillis(int backgroundPollIntervalMillis) { + this.backgroundPollIntervalMillis = backgroundPollIntervalMillis < LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS ? + LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS : backgroundPollIntervalMillis; + return this; + } + + /** + * Sets the interval between feature flag updates when the application is running in the foreground. + *

+ * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS}. That is also + * the minimum value. + * + * @param pollIntervalMillis the reconnect time base value in milliseconds + * @return the builder + * @see #backgroundPollIntervalMillis(int) + */ + public PollingDataSourceBuilder pollIntervalMillis(int pollIntervalMillis) { + this.pollIntervalMillis = pollIntervalMillis <= DEFAULT_POLL_INTERVAL_MILLIS ? + DEFAULT_POLL_INTERVAL_MILLIS : pollIntervalMillis; + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ServiceEndpointsBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ServiceEndpointsBuilder.java new file mode 100644 index 00000000..db032135 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ServiceEndpointsBuilder.java @@ -0,0 +1,242 @@ +package com.launchdarkly.sdk.android.integrations; + +import android.net.Uri; + +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.subsystems.ServiceEndpoints; + +import java.net.URI; + +/** + * Contains methods for configuring the SDK's service URIs. + *

+ * If you want to set non-default values for any of these properties, create a builder with {@link Components#serviceEndpoints()}, + * change its properties with the methods of this class, and pass it to {@link LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + *

+ * The default behavior, if you do not change any of these properties, is that the SDK will connect to the standard endpoints + * in the LaunchDarkly production service. There are several use cases for changing these properties: + *

    + *
  • You are using the LaunchDarkly Relay Proxy. + * In this case, set {@link #relayProxy(URI)}. + *
  • You are connecting to a private instance of LaunchDarkly, rather than the standard production services. + * In this case, there will be custom base URIs for each service, so you must set {@link #streaming(URI)}, + * {@link #polling(URI)}, and {@link #events(URI)}. + *
  • You are connecting to a test fixture that simulates the service endpoints. In this case, you may set the + * base URIs to whatever you want, although the SDK will still set the URI paths to the expected paths for + * LaunchDarkly services. + *
+ *

+ * Each of the setter methods can be called with either a {@link URI} or an equivalent string. + * Passing a string that is not a valid URI will cause an immediate {@link IllegalArgumentException}. + *

+ * If you are using a private instance and you set some of the base URIs, but not all of them, the SDK + * will log an error and may not work properly. The only exception is if you have explicitly disabled + * the SDK's use of one of the services: for instance, if you have disabled analytics events, you do + * not have to set {@link #events(URI)}. + * + *


+ *     // Example of specifying a Relay Proxy instance
+ *     LDConfig config = new LDConfig.Builder()
+ *         .serviceEndpoints(
+ *             Components.serviceEndpoints()
+ *                 .relayProxy("http://my-relay-hostname:80")
+ *         )
+ *         .build();
+ *
+ *     // Example of specifying a private LaunchDarkly instance
+ *     LDConfig config = new LDConfig.Builder()
+ *         .serviceEndpoints(
+ *             Components.serviceEndpoints()
+ *                 .streaming("https://stream.mycompany.launchdarkly.com")
+ *                 .polling("https://app.mycompany.launchdarkly.com")
+ *                 .events("https://events.mycompany.launchdarkly.com"))
+ *         )
+ *         .build();
+ * 
+ * + * @since 3.3.0 + */ +public abstract class ServiceEndpointsBuilder { + protected URI streamingBaseUri; + protected URI pollingBaseUri; + protected URI eventsBaseUri; + + /** + * Sets a custom base URI for the events service. + *

+ * You should only call this method if you are using a private instance or test fixture + * (see {@link ServiceEndpointsBuilder}). If you are using the LaunchDarkly Relay Proxy, + * call {@link #relayProxy(URI)} instead. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *       .serviceEndpoints(
+     *           Components.serviceEndpoints()
+     *               .streaming("https://stream.mycompany.launchdarkly.com")
+     *               .polling("https://app.mycompany.launchdarkly.com")
+     *               .events("https://events.mycompany.launchdarkly.com")
+     *       )
+     *       .build();
+     * 
+ * + * @param eventsBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder events(URI eventsBaseUri) { + this.eventsBaseUri = eventsBaseUri; + return this; + } + + /** + * Equivalent to {@link #events(URI)}, specifying the URI as a string. + * @param eventsBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder events(String eventsBaseUri) { + return events(eventsBaseUri == null ? null : URI.create(eventsBaseUri)); + } + + /** + * Equivalent to {@link #events(URI)}, specifying the URI as an {@code android.net.Uri}. + * @param eventsBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder events(Uri eventsBaseUri) { + return events(eventsBaseUri == null ? null : URI.create(eventsBaseUri.toString())); + } + + /** + * Sets a custom base URI for the polling service. + *

+ * You should only call this method if you are using a private instance or test fixture + * (see {@link ServiceEndpointsBuilder}). If you are using the LaunchDarkly Relay Proxy, + * call {@link #relayProxy(URI)} instead. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *       .serviceEndpoints(
+     *           Components.serviceEndpoints()
+     *               .streaming("https://stream.mycompany.launchdarkly.com")
+     *               .polling("https://app.mycompany.launchdarkly.com")
+     *               .events("https://events.mycompany.launchdarkly.com")
+     *       )
+     *       .build();
+     * 
+ * + * @param pollingBaseUri the base URI of the polling service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder polling(URI pollingBaseUri) { + this.pollingBaseUri = pollingBaseUri; + return this; + } + + /** + * Equivalent to {@link #polling(URI)}, specifying the URI as a string. + * @param pollingBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder polling(String pollingBaseUri) { + return polling(pollingBaseUri == null ? null : URI.create(pollingBaseUri)); + } + + /** + * Equivalent to {@link #polling(URI)}, specifying the URI as an {@code android.net.Uri}. + * @param pollingBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder polling(Uri pollingBaseUri) { + return polling(pollingBaseUri == null ? null : URI.create(pollingBaseUri.toString())); + } + + /** + * Specifies a single base URI for a Relay Proxy instance. + *

+ * When using the LaunchDarkly Relay Proxy, the SDK only needs to know the single base URI + * of the Relay Proxy, which will provide all the proxied service endpoints. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *       .serviceEndpoints(
+     *           Components.serviceEndpoints()
+     *               .relayProxy("http://my-relay-hostname:8080")
+     *       )
+     *       .build();
+     * 
+ * + * @param relayProxyBaseUri the Relay Proxy base URI, or null to reset to default endpoints + * @return the builder + */ + public ServiceEndpointsBuilder relayProxy(URI relayProxyBaseUri) { + this.eventsBaseUri = relayProxyBaseUri; + this.pollingBaseUri = relayProxyBaseUri; + this.streamingBaseUri = relayProxyBaseUri; + return this; + } + + /** + * Equivalent to {@link #relayProxy(URI)}, specifying the URI as a string. + * @param relayProxyBaseUri the Relay Proxy base URI, or null to reset to default endpoints + * @return the builder + */ + public ServiceEndpointsBuilder relayProxy(String relayProxyBaseUri) { + return relayProxy(relayProxyBaseUri == null ? null : URI.create(relayProxyBaseUri)); + } + + /** + * Equivalent to {@link #relayProxy(URI)}, specifying the URI as an {@code android.net.Uri}. + * @param relayProxyBaseUri the Relay Proxy base URI, or null to reset to default endpoints + * @return the builder + */ + public ServiceEndpointsBuilder relayProxy(Uri relayProxyBaseUri) { + return relayProxy(relayProxyBaseUri == null ? null : URI.create(relayProxyBaseUri.toString())); + } + + /** + * Sets a custom base URI for the streaming service. + *

+ * You should only call this method if you are using a private instance or test fixture + * (see {@link ServiceEndpointsBuilder}). If you are using the LaunchDarkly Relay Proxy, + * call {@link #relayProxy(URI)} instead. + *


+     *     LDConfig config = new LDConfig.Builder()
+     *       .serviceEndpoints(
+     *           Components.serviceEndpoints()
+     *               .streaming("https://stream.mycompany.launchdarkly.com")
+     *               .polling("https://app.mycompany.launchdarkly.com")
+     *               .events("https://events.mycompany.launchdarkly.com")
+     *       )
+     *       .build();
+     * 
+ * + * @param streamingBaseUri the base URI of the streaming service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder streaming(URI streamingBaseUri) { + this.streamingBaseUri = streamingBaseUri; + return this; + } + + /** + * Equivalent to {@link #streaming(URI)}, specifying the URI as a string. + * @param streamingBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder streaming(String streamingBaseUri) { + return streaming(streamingBaseUri == null ? null : URI.create(streamingBaseUri)); + } + + /** + * Equivalent to {@link #streaming(URI)}, specifying the URI as an {@code android.net.Uri}. + * @param streamingBaseUri the base URI of the events service; null to use the default + * @return the builder + */ + public ServiceEndpointsBuilder streaming(Uri streamingBaseUri) { + return streaming(streamingBaseUri == null ? null : URI.create(streamingBaseUri.toString())); + } + + /** + * Called internally by the SDK to create a configuration instance. Applications do not need + * to call this method. + * @return the configuration object + */ + abstract public ServiceEndpoints build(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingDataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingDataSourceBuilder.java new file mode 100644 index 00000000..e969dcce --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingDataSourceBuilder.java @@ -0,0 +1,70 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDConfig.Builder; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.DataSource; + +/** + * Contains methods for configuring the streaming data source. + *

+ * By default, the SDK uses a streaming connection to receive feature flag data from LaunchDarkly. If you want + * to customize the behavior of the connection, create a builder with {@link Components#streamingDataSource()}, + * change its properties with the methods of this class, and pass it to {@link Builder#dataSource(ComponentConfigurer)}: + *


+ *     LDConfig config = new LDConfig.Builder()
+ *         .dataSource(Components.streamingDataSource().initialReconnectDelayMillis(500))
+ *         .build();
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling {@link Components#streamingDataSource()}. + * + * @since 3.3.0 + */ +public abstract class StreamingDataSourceBuilder implements ComponentConfigurer { + /** + * The default value for {@link #initialReconnectDelayMillis(int)}: 1000 milliseconds. + */ + public static final int DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS = 1_000; + + protected int backgroundPollIntervalMillis = LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS; + protected int initialReconnectDelayMillis = DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS; + + /** + * Sets the interval between feature flag updates when the application is running in the background. + *

+ * Even when configured to use streaming, the SDK will switch to polling when in the background + * (unless {@link Builder#disableBackgroundUpdating(boolean)} is set). This property determines + * how often polling will happen. + *

+ * The default value is {@link LDConfig#DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS}; the minimum + * is {@link LDConfig#MIN_BACKGROUND_POLL_INTERVAL_MILLIS}. + * + * @param backgroundPollIntervalMillis the reconnect time base value in milliseconds + * @return the builder + */ + public StreamingDataSourceBuilder backgroundPollIntervalMillis(int backgroundPollIntervalMillis) { + this.backgroundPollIntervalMillis = backgroundPollIntervalMillis < LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS ? + LDConfig.MIN_BACKGROUND_POLL_INTERVAL_MILLIS : backgroundPollIntervalMillis; + return this; + } + + /** + * Sets the initial reconnect delay for the streaming connection. + *

+ * The streaming service uses a backoff algorithm (with jitter) every time the connection needs + * to be reestablished. The delay for the first reconnection will start near this value, and then + * increase exponentially for any subsequent connection failures. + *

+ * The default value is {@link #DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS}. + * + * @param initialReconnectDelayMillis the reconnect time base value in milliseconds + * @return the builder + */ + public StreamingDataSourceBuilder initialReconnectDelayMillis(int initialReconnectDelayMillis) { + this.initialReconnectDelayMillis = initialReconnectDelayMillis <= 0 ? 0 : + initialReconnectDelayMillis; + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java new file mode 100644 index 00000000..7d99e00b --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ClientContext.java @@ -0,0 +1,151 @@ +package com.launchdarkly.sdk.android.subsystems; + +import android.app.Application; + +import com.launchdarkly.logging.LDLogger; +import com.launchdarkly.sdk.android.LDClient; +import com.launchdarkly.sdk.android.LDConfig; + +/** + * Configuration information provided by the {@link com.launchdarkly.sdk.android.LDClient} when + * creating components. + *

+ * The getter methods in this class provide information about the initial configuration of the + * client. This includes properties from {@link LDConfig}, and also values that are computed + * during initialization. It is preferable for components to copy properties from this class rather + * than to retain a reference to the entire {@link LDConfig} object. + *

+ * The actual implementation class may contain other properties that are only relevant to the built-in + * SDK components and are therefore not part of this base class; this allows the SDK to add its own + * context information as needed without disturbing the public API. + *

+ * All properties of this object are immutable; they are set at initialization time and do not + * reflect any later state changes in the client. + * + * @since 3.3.0 + */ +public class ClientContext { + private final Application application; + private final LDLogger baseLogger; + private final LDConfig config; + private final boolean evaluationReasons; + private final String environmentName; + private final HttpConfiguration http; + private final boolean initiallySetOffline; + private final String mobileKey; + private final ServiceEndpoints serviceEndpoints; + + public ClientContext( + Application application, + String mobileKey, + LDLogger baseLogger, + LDConfig config, + String environmentName, + boolean evaluationReasons, + HttpConfiguration http, + boolean initiallySetOffline, + ServiceEndpoints serviceEndpoints + ) { + this.application = application; + this.mobileKey = mobileKey; + this.baseLogger = baseLogger; + this.config = config; + this.environmentName = environmentName; + this.evaluationReasons = evaluationReasons; + this.http = http; + this.initiallySetOffline = initiallySetOffline; + this.serviceEndpoints = serviceEndpoints; + } + + protected ClientContext(ClientContext copyFrom) { + this( + copyFrom.application, + copyFrom.mobileKey, + copyFrom.baseLogger, + copyFrom.config, + copyFrom.environmentName, + copyFrom.evaluationReasons, + copyFrom.http, + copyFrom.initiallySetOffline, + copyFrom.serviceEndpoints + ); + } + + /** + * The Android application object. + * @return the application + */ + public Application getApplication() { + return application; + } + + /** + * The base logger for the SDK. + * @return a logger instance + */ + public LDLogger getBaseLogger() { + return baseLogger; + } + + /** + * Returns the full configuration object. THIS IS A TEMPORARY METHOD that will be removed prior + * to release-- the goal is to NOT retain the full LDConfig in these components, but until we + * have moved more of the config properties into subconfiguration builders, this is necessary. + * @return the configuration object + */ + public LDConfig getConfig() { + return config; + } + + /** + * Returns the configured environment name. + * @return the environment name + */ + public String getEnvironmentName() { + return environmentName; + } + + /** + * Returns true if evaluation reasons are enabled. + * @return true if evaluation reasons are enabled + */ + public boolean isEvaluationReasons() { + return evaluationReasons; + } + + /** + * Returns the HTTP configuration. + * @return the HTTP configuration + */ + public HttpConfiguration getHttp() { + return http; + } + + /** + * Returns true if the initial configuration specified that the SDK should be offline. + * @return true if initially set to be offline + */ + public boolean isInitiallySetOffline() { + return initiallySetOffline; + } + + /** + * Returns the configured mobile key. + *

+ * In multi-environment mode, there is a separate {@link ClientContext} for each environment, + * corresponding to the {@link LDClient} instance for that environment. + * + * @return the mobile key + */ + public String getMobileKey() { + return mobileKey; + } + + /** + * Returns the base service URIs used by SDK components. + * @return the service endpoint URIs + */ + public ServiceEndpoints getServiceEndpoints() { + return serviceEndpoints; + } +} \ No newline at end of file diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ComponentConfigurer.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ComponentConfigurer.java new file mode 100644 index 00000000..7a25e5de --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ComponentConfigurer.java @@ -0,0 +1,20 @@ +package com.launchdarkly.sdk.android.subsystems; + +/** + * The common interface for SDK component factories and configuration builders. Applications should not + * need to implement this interface. + * + * @param the type of SDK component or configuration object being constructed + * @since 3.3.0 + */ +public interface ComponentConfigurer { + /** + * Called internally by the SDK to create an implementation instance. Applications should not need + * to call this method. + * + * @param clientContext provides configuration properties and other components from the current + * SDK client instance + * @return a instance of the component type + */ + T build(ClientContext clientContext); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSource.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSource.java new file mode 100644 index 00000000..24427e82 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DataSource.java @@ -0,0 +1,42 @@ +package com.launchdarkly.sdk.android.subsystems; + +import com.launchdarkly.sdk.android.Components; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.LDConfig.Builder; + +/** + * An object that describes how the SDK will obtain feature flag data from LaunchDarkly. + *

+ * Currently, this is a simple container for configuration properties. In the future, it will become + * a real component interface allowing for custom behavior, as it is in the server-side Java SDK. + * + * @since 3.3.0 + * @see Components#streamingDataSource() + * @see Components#pollingDataSource() + * @see LDConfig.Builder#dataSource(ComponentConfigurer) + */ +public interface DataSource { + /** + * Returns true if streaming is disabled. + * @return true if streaming is disabled + */ + boolean isStreamingDisabled(); + + /** + * Returns the configured background polling interval. + * @return the background polling interval in milliseconds + */ + int getBackgroundPollIntervalMillis(); + + /** + * Returns the configured initial stream reconnect delay. + * @return the initial stream reconnect delay in milliseconds, or zero if streaming is disabled + */ + int getInitialReconnectDelayMillis(); + + /** + * Returns the configured foreground polling interval. + * @return the foreground polling interval in milliseconds, or zero if streaming is enabled + */ + int getPollIntervalMillis(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DiagnosticDescription.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DiagnosticDescription.java new file mode 100644 index 00000000..601693a8 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/DiagnosticDescription.java @@ -0,0 +1,27 @@ +package com.launchdarkly.sdk.android.subsystems; + +import com.launchdarkly.sdk.LDValue; + +/** + * Optional interface for components to describe their own configuration. + *

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

+ * The {@link #describeConfiguration(ClientContext)} method should return either null or a JSON value. For + * custom components, the value must be a string that describes the basic nature of this component + * implementation (e.g. "Redis"). Built-in LaunchDarkly components may instead return a JSON object + * containing multiple properties specific to the LaunchDarkly diagnostic schema. + * + * @since 3.3.0 + */ +public interface DiagnosticDescription { + /** + * Used internally by the SDK to inspect the configuration. + * @param clientContext allows access to the client configuration + * @return an {@link LDValue} or null + */ + LDValue describeConfiguration(ClientContext clientContext); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java new file mode 100644 index 00000000..4a093e6e --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/EventProcessor.java @@ -0,0 +1,116 @@ +package com.launchdarkly.sdk.android.subsystems; + +import com.launchdarkly.sdk.EvaluationDetail; +import com.launchdarkly.sdk.EvaluationReason; +import com.launchdarkly.sdk.LDUser; +import com.launchdarkly.sdk.LDValue; + +import java.io.Closeable; + +/** + * Interface for an object that can send or store analytics events. + *

+ * Application code normally does not need to interact with this interface. It is provided + * to allow a custom implementation or test fixture to be substituted for the SDK's normal + * analytics event logic. + * + * @since 3.3.0 + */ +public interface EventProcessor extends Closeable { + /** + * Constant used with {@link #recordEvaluationEvent}. + */ + public static final int NO_VERSION = -1; + + /** + * Records the action of evaluating a feature flag. + *

+ * Depending on the feature flag properties and event properties, this may be transmitted to + * the events service as an individual event, or may only be added into summary data. + * + * @param user the current user + * @param flagKey key of the feature flag that was evaluated + * @param flagVersion the version of the flag, or {@link #NO_VERSION} if the flag was not found + * @param variation the result variation index, or {@link EvaluationDetail#NO_VARIATION} if evaluation failed + * @param value the result value + * @param reason the evaluation reason, or null if the reason was not requested + * @param defaultValue the default value parameter for the evaluation + * @param requireFullEvent true if full-fidelity analytics events should be sent for this flag + * @param debugEventsUntilDate if non-null, debug events are to be generated until this millisecond time + */ + void recordEvaluationEvent( + LDUser user, + String flagKey, + int flagVersion, + int variation, + LDValue value, + EvaluationReason reason, + LDValue defaultValue, + boolean requireFullEvent, + Long debugEventsUntilDate + ); + + /** + * Registers an evaluation context, as when the SDK's {@code identify} method is called. + * + * @param user the current user + */ + void recordIdentifyEvent( + LDUser user + ); + + /** + * Creates a custom event, as when the SDK's {@code track} method is called. + * + * @param user the current user + * @param eventKey the event key + * @param data optional custom data provided for the event, may be null or {@link LDValue#ofNull()} if not used + * @param metricValue optional numeric metric value provided for the event, or null + */ + void recordCustomEvent( + LDUser user, + String eventKey, + LDValue data, + Double metricValue + ); + + /** + * Creates an alias event, as when the SDK's {@code alias} method is called. + * + * @param user the current user + * @param previousUser the previous user + */ + void recordAliasEvent( + LDUser user, + LDUser previousUser + ); + + /** + * Starts any periodic tasks used by the event processor. + */ + void start(); + + /** + * Stops any periodic tasks used by the event processor. + */ + void stop(); + + /** + * Puts the event processor into offline mode if appropriate + * @param offline true if the SDK has been put offline + */ + void setOffline(boolean offline); + + /** + * Specifies that any buffered events should be sent as soon as possible, rather than waiting + * for the next flush interval. This method is asynchronous, so events still may not be sent + * until a later time. However, calling {@link Closeable#close()} will synchronously deliver + * any events that were not yet delivered prior to shutting down. + */ + void flush(); + + /** + * Specifies that any buffered events should be sent immediately, blocking until done. + */ + void blockingFlush(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HttpConfiguration.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HttpConfiguration.java new file mode 100644 index 00000000..a0257cc2 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/HttpConfiguration.java @@ -0,0 +1,87 @@ +package com.launchdarkly.sdk.android.subsystems; + +import com.launchdarkly.sdk.android.LDHeaderUpdater; +import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; + +import java.util.HashMap; +import java.util.Map; + +import static java.util.Collections.emptyMap; + +/** + * Encapsulates top-level HTTP configuration that applies to all SDK components. + *

+ * Use {@link HttpConfigurationBuilder} to construct an instance. + *

+ * The SDK's built-in components use OkHttp as the HTTP client implementation, but since OkHttp types + * are not surfaced in the public API and custom components might use some other implementation, this + * class only provides the properties that would be used to create an HTTP client; it does not create + * the client itself. SDK implementation code uses its own helper methods to do so. + * + * @since 3.3.0 + */ +public final class HttpConfiguration { + private final int connectTimeoutMillis; + private final Map defaultHeaders; + private final LDHeaderUpdater headerTransform; + private final boolean useReport; + + /** + * Creates an instance. + * + * @param connectTimeoutMillis see {@link #getConnectTimeoutMillis()} + * @param defaultHeaders see {@link #getDefaultHeaders()} + * @param headerTransform see {@link #getHeaderTransform()} + * @param useReport see {@link #isUseReport()} + */ + public HttpConfiguration( + int connectTimeoutMillis, + Map defaultHeaders, + LDHeaderUpdater headerTransform, + boolean useReport + ) { + super(); + this.connectTimeoutMillis = connectTimeoutMillis; + this.defaultHeaders = defaultHeaders == null ? emptyMap() : new HashMap<>(defaultHeaders); + this.headerTransform = headerTransform; + this.useReport = useReport; + } + + /** + * The connection timeout. This is the time allowed for the underlying HTTP client to connect + * to the LaunchDarkly server. + * + * @return the connection timeout in milliseconds + */ + public int getConnectTimeoutMillis() { + return connectTimeoutMillis; + } + + /** + * Returns the basic headers that should be added to all HTTP requests from SDK components to + * LaunchDarkly services, based on the current SDK configuration. + * + * @return a list of HTTP header names and values + */ + public Iterable> getDefaultHeaders() { + return defaultHeaders.entrySet(); + } + + /** + * Returns the callback for modifying request headers, if any. + * + * @return the callback for modifying request headers + */ + public LDHeaderUpdater getHeaderTransform() { + return headerTransform; + } + + /** + * The setting for whether to use the HTTP REPORT method. + * + * @return true to use HTTP REPORT + */ + public boolean isUseReport() { + return useReport; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ServiceEndpoints.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ServiceEndpoints.java new file mode 100644 index 00000000..1c5252db --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/subsystems/ServiceEndpoints.java @@ -0,0 +1,53 @@ +package com.launchdarkly.sdk.android.subsystems; + +import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; +import java.net.URI; + +/** + * Specifies the base service URIs used by SDK components. + *

+ * See {@link ServiceEndpointsBuilder} for more details on these properties. + * + * @since 3.3.0 + */ +public final class ServiceEndpoints { + private final URI streamingBaseUri; + private final URI pollingBaseUri; + private final URI eventsBaseUri; + + /** + * Used internally by the SDK to store service endpoints. + * @param streamingBaseUri the base URI for the streaming service + * @param pollingBaseUri the base URI for the polling service + * @param eventsBaseUri the base URI for the events service + */ + public ServiceEndpoints(URI streamingBaseUri, URI pollingBaseUri, URI eventsBaseUri) { + this.streamingBaseUri = streamingBaseUri; + this.pollingBaseUri = pollingBaseUri; + this.eventsBaseUri = eventsBaseUri; + } + + /** + * The base URI for the streaming service. + * @return the base URI, or null + */ + public URI getStreamingBaseUri() { + return streamingBaseUri; + } + + /** + * The base URI for the polling service. + * @return the base URI, or null + */ + public URI getPollingBaseUri() { + return pollingBaseUri; + } + + /** + * The base URI for the events service. + * @return the base URI, or null + */ + public URI getEventsBaseUri() { + return eventsBaseUri; + } +}