diff --git a/CHANGELOG.md b/CHANGELOG.md index 11801849..892e4270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.14.0] - 2020-12-17 +### Added +- Added `LDConfig.Builder.setPollUri` configuration setter that is equivalent to the now deprecated `setBaseUri`. +- Added `LDConfig.getPollUri` configuration getter that is equivalent to the now deprecated `getPollUri`. +- Added `LDClient.doubleVariation` for getting floating point flag values as a `double`. This is preferred over the now deprecated `floatVariation`. +### Fixed +- Improved event summarization logic to avoid potential runtime exceptions. Thanks to @yzheng988 for reporting ([#105](https://github.com/launchdarkly/android-client-sdk/issues/105)). +- Internal throttling logic would sometimes delay new poll or stream connections even when there were no recent connections. This caused switching active user contexts using `identify` to sometimes delay retrieving the most recent flags, and therefore delay the completion of the returned `Future`. +### Changed +- The maximum delay the internal throttling logic could delay a flag request has been reduced to 60 seconds. +### Deprecated +- Deprecated `LDConfig.Builder.setBaseUri` and `LDConfig.getBaseUri`, please use `setPollUri` and `getPollUri` instead. +- Deprecated `LDClient.floatVariation`, please use `doubleVariation` for evaluating flags with floating point values. + ## [2.13.0] - 2020-08-07 ### Added - Allow specifying additional headers to be included on HTTP requests to LaunchDarkly services using `LDConfig.Builder.setAdditionalHeaders`. This feature is to enable certain proxy configurations, and is not needed for normal use. diff --git a/example/build.gradle b/example/build.gradle index 5eb50c50..eb2a0c8e 100644 --- a/example/build.gradle +++ b/example/build.gradle @@ -46,7 +46,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:26.1.0' implementation project(path: ':launchdarkly-android-client-sdk') // Comment the previous line and uncomment this one to depend on the published artifact: - //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.13.0' + //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.14.0' implementation 'com.jakewharton.timber:timber:4.7.1' diff --git a/launchdarkly-android-client-sdk/build.gradle b/launchdarkly-android-client-sdk/build.gradle index e24fa579..cbffb35e 100644 --- a/launchdarkly-android-client-sdk/build.gradle +++ b/launchdarkly-android-client-sdk/build.gradle @@ -7,7 +7,7 @@ apply plugin: 'io.codearte.nexus-staging' allprojects { group = 'com.launchdarkly' - version = '2.13.0' + version = '2.14.0' sourceCompatibility = 1.7 targetCompatibility = 1.7 } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DiagnosticEventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DiagnosticEventTest.java index 3187c065..481c7c2e 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DiagnosticEventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DiagnosticEventTest.java @@ -57,7 +57,7 @@ public void testCustomDiagnosticConfiguration() { .setDisableBackgroundUpdating(true) .setBackgroundPollingIntervalMillis(900_000) .setConnectionTimeoutMillis(5_000) - .setBaseUri(Uri.parse("https://1.1.1.1")) + .setPollUri(Uri.parse("https://1.1.1.1")) .setEventsUri(Uri.parse("https://1.1.1.1")) .setStreamUri(Uri.parse("https://1.1.1.1")) .setDiagnosticRecordingIntervalMillis(1_800_000) diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDClientTest.java index 3d408945..2196cb8f 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDClientTest.java @@ -68,7 +68,9 @@ public void testOfflineClientReturnsFallbacks() { assertTrue(ldClient.isOffline()); assertTrue(ldClient.boolVariation("boolFlag", true)); + //noinspection deprecation assertEquals(1.0F, ldClient.floatVariation("floatFlag", 1.0F)); + assertEquals(1.5, ldClient.doubleVariation("floatFlag", 1.5)); assertEquals(Integer.valueOf(1), ldClient.intVariation("intFlag", 1)); assertEquals("fallback", ldClient.stringVariation("stringFlag", "fallback")); @@ -86,6 +88,7 @@ public void givenFallbacksAreNullAndTestOfflineClientReturnsFallbacks() { assertNull(ldClient.jsonVariation("jsonFlag", null)); assertNull(ldClient.boolVariation("boolFlag", null)); + //noinspection deprecation assertNull(ldClient.floatVariation("floatFlag", null)); assertNull(ldClient.intVariation("intFlag", null)); assertNull(ldClient.stringVariation("stringFlag", null)); @@ -179,6 +182,7 @@ public void run() { assertTrue(ldClient.isOffline()); assertTrue(ldClient.boolVariation("boolFlag", true)); + //noinspection deprecation assertEquals(1.0F, ldClient.floatVariation("floatFlag", 1.0F)); assertEquals(Integer.valueOf(1), ldClient.intVariation("intFlag", 1)); assertEquals("fallback", ldClient.stringVariation("stringFlag", "fallback")); @@ -207,7 +211,9 @@ public void eventGenerationDoesNotCrash() throws IOException { // Do a variety of evaluations assertTrue(ldClient.boolVariation("boolFlag", true)); + //noinspection deprecation assertEquals(1.0F, ldClient.floatVariation("floatFlag", 1.0F)); + assertEquals(1.5, ldClient.doubleVariation("doubleFlag", 1.5)); assertEquals(Integer.valueOf(1), ldClient.intVariation("intFlag", 1)); assertEquals("fallback", ldClient.stringVariation("stringFlag", "fallback")); @@ -216,6 +222,7 @@ public void eventGenerationDoesNotCrash() throws IOException { assertEquals(expectedJson, ldClient.jsonVariation("jsonFlag", expectedJson)); assertNull(ldClient.boolVariation("boolFlag", null)); + //noinspection deprecation assertNull(ldClient.floatVariation("floatFlag", null)); assertNull(ldClient.intVariation("intFlag", null)); assertNull(ldClient.stringVariation("stringFlag", null)); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDConfigTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDConfigTest.java index f0695cb7..03c94e23 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDConfigTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDConfigTest.java @@ -28,7 +28,9 @@ public void testBuilderDefaults() { assertTrue(config.isStream()); assertFalse(config.isOffline()); - assertEquals(LDConfig.DEFAULT_BASE_URI, config.getBaseUri()); + //noinspection deprecation + assertEquals(LDConfig.DEFAULT_POLL_URI, config.getBaseUri()); + assertEquals(LDConfig.DEFAULT_POLL_URI, config.getPollUri()); assertEquals(LDConfig.DEFAULT_EVENTS_URI, config.getEventsUri()); assertEquals(LDConfig.DEFAULT_STREAM_URI, config.getStreamUri()); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/MultiEnvironmentLDClientTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/MultiEnvironmentLDClientTest.java index b25b9c78..7cf3fdb1 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/MultiEnvironmentLDClientTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/MultiEnvironmentLDClientTest.java @@ -60,7 +60,9 @@ public void testOfflineClientReturnsFallbacks() { assertTrue(ldClient.isOffline()); assertTrue(ldClient.boolVariation("boolFlag", true)); + //noinspection deprecation assertEquals(1.0F, ldClient.floatVariation("floatFlag", 1.0F)); + assertEquals(1.5, ldClient.doubleVariation("doubleFlag", 1.5)); assertEquals(Integer.valueOf(1), ldClient.intVariation("intFlag", 1)); assertEquals("fallback", ldClient.stringVariation("stringFlag", "fallback")); @@ -76,8 +78,8 @@ public void givenFallbacksAreNullAndTestOfflineClientReturnsFallbacks() { assertTrue(ldClient.isInitialized()); assertTrue(ldClient.isOffline()); assertNull(ldClient.jsonVariation("jsonFlag", null)); - assertNull(ldClient.boolVariation("boolFlag", null)); + //noinspection deprecation assertNull(ldClient.floatVariation("floatFlag", null)); assertNull(ldClient.intVariation("intFlag", null)); assertNull(ldClient.stringVariation("stringFlag", null)); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsSummaryEventStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsSummaryEventStoreTest.java index f1275078..675eb735 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsSummaryEventStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsSummaryEventStoreTest.java @@ -89,7 +89,9 @@ public void evaluationsAreSaved() { ldClient.boolVariation("boolFlag", true); ldClient.jsonVariation("jsonFlag", new JsonObject()); + //noinspection deprecation ldClient.floatVariation("floatFlag", 0.1f); + ldClient.doubleVariation("doubleFlag", 0.2); ldClient.intVariation("intFlag", 6); ldClient.stringVariation("stringFlag", "string"); @@ -98,12 +100,14 @@ public void evaluationsAreSaved() { Assert.assertTrue(features.keySet().contains("boolFlag")); Assert.assertTrue(features.keySet().contains("jsonFlag")); Assert.assertTrue(features.keySet().contains("floatFlag")); + Assert.assertTrue(features.keySet().contains("doubleFlag")); Assert.assertTrue(features.keySet().contains("intFlag")); Assert.assertTrue(features.keySet().contains("stringFlag")); Assert.assertEquals(true, features.get("boolFlag").getAsJsonObject().get("default").getAsBoolean()); Assert.assertEquals(new JsonObject(), features.get("jsonFlag").getAsJsonObject().get("default").getAsJsonObject()); Assert.assertEquals(0.1f, features.get("floatFlag").getAsJsonObject().get("default").getAsFloat()); + Assert.assertEquals(0.2, features.get("doubleFlag").getAsJsonObject().get("default").getAsDouble()); Assert.assertEquals(6, features.get("intFlag").getAsJsonObject().get("default").getAsInt()); Assert.assertEquals("string", features.get("stringFlag").getAsJsonObject().get("default").getAsString()); } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/ThrottlerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/ThrottlerTest.java index 64411ae4..d1ec7629 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/ThrottlerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/ThrottlerTest.java @@ -10,32 +10,58 @@ import java.util.concurrent.atomic.AtomicBoolean; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; @RunWith(AndroidJUnit4.class) public class ThrottlerTest { - private final AtomicBoolean hasRun = new AtomicBoolean(false); private Throttler throttler; - private static final long MAX_RETRY_TIME_MS = 600000; - private static final long RETRY_TIME_MS = 1000; + private final AtomicBoolean hasRun = new AtomicBoolean(false); + private final long MAX_RETRY_TIME_MS = 30_000; @Before public void setUp() { - throttler = new Throttler(new Runnable() { + hasRun.set(false); + throttler = new Throttler(new Runnable() { @Override public void run() { hasRun.set(true); } - }, RETRY_TIME_MS, MAX_RETRY_TIME_MS); + }, 1_000, MAX_RETRY_TIME_MS); + } + + @Test + public void initialRunsInstant() { + throttler.attemptRun(); + assertTrue(hasRun.get()); + + // Second run is instant on fresh throttler to not penalize `init`. + hasRun.set(false); + throttler.attemptRun(); + assertTrue(hasRun.get()); + // Third run should be delayed + hasRun.set(false); + throttler.attemptRun(); + assertFalse(hasRun.get()); } @Test - public void testFirstRunIsInstant() { + public void delaysResetThrottle() throws InterruptedException { + throttler.attemptRun(); + throttler.attemptRun(); + Thread.sleep(1_500); + + // Delay should allow third run to be instant + hasRun.set(false); + throttler.attemptRun(); + assertTrue(hasRun.get()); + + // Confirms second run after delay is throttled + hasRun.set(false); throttler.attemptRun(); - boolean result = this.hasRun.getAndSet(false); - assertTrue(result); + assertFalse(hasRun.get()); } @Ignore("Useful for inspecting jitter values empirically") diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityManager.java index 6bc3c376..6fc0262e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityManager.java @@ -15,7 +15,7 @@ class ConnectivityManager { - private static final long MAX_RETRY_TIME_MS = 3_600_000; // 1 hour + private static final long MAX_RETRY_TIME_MS = 60_000; // 60 seconds private static final long RETRY_TIME_MS = 1_000; // 1 second private final ConnectionMode foregroundMode; @@ -31,7 +31,7 @@ class ConnectivityManager { private final Foreground.Listener foregroundListener; private final String environmentName; private final int pollingInterval; - private LDUtil.ResultCallback monitor; + private final LDUtil.ResultCallback monitor; private LDUtil.ResultCallback initCallback = null; private volatile boolean initialized = false; private volatile boolean setOffline; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DiagnosticEvent.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DiagnosticEvent.java index 3719d6df..43814000 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DiagnosticEvent.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DiagnosticEvent.java @@ -45,7 +45,7 @@ static class DiagnosticConfiguration { private final int maxCachedUsers; DiagnosticConfiguration(LDConfig config) { - this.customBaseURI = !LDConfig.DEFAULT_BASE_URI.equals(config.getBaseUri()); + 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(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/HttpFeatureFlagFetcher.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/HttpFeatureFlagFetcher.java index 8efe6af8..ebfb9878 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/HttpFeatureFlagFetcher.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/HttpFeatureFlagFetcher.java @@ -118,7 +118,7 @@ public void onResponse(@NonNull Call call, @NonNull final Response response) { } private Request getDefaultRequest(LDUser user) { - String uri = config.getBaseUri() + "/msdk/evalx/users/" + user.getAsUrlSafeBase64(); + String uri = config.getPollUri() + "/msdk/evalx/users/" + user.getAsUrlSafeBase64(); if (config.isEvaluationReasons()) { uri += "?withReasons=true"; } @@ -129,7 +129,7 @@ private Request getDefaultRequest(LDUser user) { } private Request getReportRequest(LDUser user) { - String reportUri = config.getBaseUri() + "/msdk/evalx/user"; + String reportUri = config.getPollUri() + "/msdk/evalx/user"; if (config.isEvaluationReasons()) { reportUri += "?withReasons=true"; } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClient.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClient.java index 5cc657a9..3a3d3bdf 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClient.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClient.java @@ -360,15 +360,27 @@ public EvaluationDetail intVariationDetail(String flagKey, Integer fall } @Override + @Deprecated public Float floatVariation(String flagKey, Float fallback) { return variationDetailInternal(flagKey, fallback, ValueTypes.FLOAT, false).getValue(); } @Override + @Deprecated public EvaluationDetail floatVariationDetail(String flagKey, Float fallback) { return variationDetailInternal(flagKey, fallback, ValueTypes.FLOAT, true); } + @Override + public double doubleVariation(String flagKey, double fallback) { + return variationDetailInternal(flagKey, fallback, ValueTypes.DOUBLE, false).getValue(); + } + + @Override + public EvaluationDetail doubleVariationDetail(String flagKey, double fallback) { + return variationDetailInternal(flagKey, fallback, ValueTypes.DOUBLE, true); + } + @Override public String stringVariation(String flagKey, String fallback) { // TODO(gwhelanld): Change to ValueTypes.String in 3.0.0 diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClientInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClientInterface.java index de700983..a86c0319 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClientInterface.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDClientInterface.java @@ -170,8 +170,10 @@ public interface LDClientInterface extends Closeable { * * @param flagKey key for the flag to evaluate * @param fallback fallback value in case of errors evaluating the flag + * @deprecated Please use {@link #doubleVariation(String, double)} instead. * @return value of the flag or fallback */ + @Deprecated Float floatVariation(String flagKey, Float fallback); /** @@ -184,11 +186,39 @@ public interface LDClientInterface extends Closeable { * @param flagKey key for the flag to evaluate * @param fallback fallback value in case of errors evaluating the flag (see {@link #floatVariation(String, Float)}) * @return an {@link EvaluationDetail} object containing the value and other information. - * + * @deprecated Please use {@link #doubleVariationDetail(String, double)} instead. * @since 2.7.0 */ + @Deprecated EvaluationDetail floatVariationDetail(String flagKey, Float fallback); + /** + * Returns the flag value for the current user. Returns fallback when one of the following occurs: + *
    + *
  1. Flag is missing
  2. + *
  3. The flag is not of a numeric type
  4. + *
  5. Any other error
  6. + *
+ * + * @param flagKey key for the flag to evaluate + * @param fallback fallback value in case of errors evaluating the flag + * @return value of the flag or fallback + */ + double doubleVariation(String flagKey, double fallback); + + /** + * Returns the flag value for the current user, along with information about how it was calculated. + * + * Note that this will only work if you have set {@code evaluationReasons} to true with + * {@link LDConfig.Builder#setEvaluationReasons(boolean)}. Otherwise, the {@code reason} property of the result + * will be null. + * + * @param flagKey key for the flag to evaluate + * @param fallback fallback value in case of errors evaluating the flag (see {@link #doubleVariation(String, double)}) + * @return an {@link EvaluationDetail} object containing the value and other information. + */ + EvaluationDetail doubleVariationDetail(String flagKey, double fallback); + /** * Returns the flag value for the current user. Returns fallback when one of the following occurs: *
    diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDConfig.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDConfig.java index b55ff9be..e951b145 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDConfig.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDConfig.java @@ -30,7 +30,7 @@ public class LDConfig { static final String primaryEnvironmentName = "default"; - static final Uri DEFAULT_BASE_URI = Uri.parse("https://app.launchdarkly.com"); + static final Uri DEFAULT_POLL_URI = Uri.parse("https://app.launchdarkly.com"); static final Uri DEFAULT_EVENTS_URI = Uri.parse("https://mobile.launchdarkly.com/mobile"); static final Uri DEFAULT_STREAM_URI = Uri.parse("https://clientstream.launchdarkly.com"); @@ -47,7 +47,7 @@ public class LDConfig { private final Map mobileKeys; - private final Uri baseUri; + private final Uri pollUri; private final Uri eventsUri; private final Uri streamUri; @@ -80,7 +80,7 @@ public class LDConfig { private final Map additionalHeaders; LDConfig(Map mobileKeys, - Uri baseUri, + Uri pollUri, Uri eventsUri, Uri streamUri, int eventsCapacity, @@ -104,7 +104,7 @@ public class LDConfig { Map additionalHeaders) { this.mobileKeys = mobileKeys; - this.baseUri = baseUri; + this.pollUri = pollUri; this.eventsUri = eventsUri; this.streamUri = streamUri; this.eventsCapacity = eventsCapacity; @@ -182,8 +182,24 @@ public Map getMobileKeys() { return mobileKeys; } + /** + * Get the currently configured URI for polling requests. + * + * @return the base URI configured to be used for poll requests. + * @deprecated Please use {@link #getPollUri()} instead. + */ + @Deprecated public Uri getBaseUri() { - return baseUri; + return pollUri; + } + + /** + * Get the currently configured base URI for polling requests. + * + * @return the base URI configured to be used for poll requests. + */ + public Uri getPollUri() { + return pollUri; } public Uri getEventsUri() { @@ -288,7 +304,7 @@ public static class Builder { private String mobileKey; private Map secondaryMobileKeys; - private Uri baseUri = DEFAULT_BASE_URI; + private Uri pollUri = DEFAULT_POLL_URI; private Uri eventsUri = DEFAULT_EVENTS_URI; private Uri streamUri = DEFAULT_STREAM_URI; @@ -403,9 +419,22 @@ public LDConfig.Builder setUseReport(boolean useReport) { * * @param baseUri the URI of the main LaunchDarkly service * @return the builder + * @deprecated Please use {@link #setPollUri(Uri)} instead. */ + @Deprecated public LDConfig.Builder setBaseUri(Uri baseUri) { - this.baseUri = baseUri; + this.pollUri = baseUri; + return this; + } + + /** + * Set the base URI for polling requests to LaunchDarkly. You probably don't need to set this unless instructed by LaunchDarkly. + * + * @param pollUri the URI of the main LaunchDarkly service + * @return the builder + */ + public LDConfig.Builder setPollUri(Uri pollUri) { + this.pollUri = pollUri; return this; } @@ -713,7 +742,7 @@ public LDConfig build() { return new LDConfig( mobileKeys, - baseUri, + pollUri, eventsUri, streamUri, eventsCapacity, diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsSummaryEventStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsSummaryEventStore.java index 50fe9808..c350779d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsSummaryEventStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsSummaryEventStore.java @@ -4,12 +4,14 @@ import android.app.Application; import android.content.Context; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; import com.google.gson.JsonParser; import com.google.gson.JsonPrimitive; @@ -78,9 +80,9 @@ public synchronized void addOrUpdateEvent(String flagResponseKey, JsonElement va String flagSummary = object.toString(); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(flagResponseKey, object.toString()); - editor.apply(); + sharedPreferences.edit() + .putString(flagResponseKey, object.toString()) + .apply(); Timber.d("Updated summary for flagKey %s to %s", flagResponseKey, flagSummary); } @@ -90,20 +92,34 @@ public synchronized SummaryEvent getSummaryEvent() { return getSummaryEventNoSync(); } - private SummaryEvent getSummaryEventNoSync() { - JsonObject features = getFeaturesJsonObject(); - if (features.keySet().size() == 0) { - return null; + private static Long removeStartDateFromFeatureSummary(@NonNull JsonObject featureSummary) { + JsonElement startDate = featureSummary.remove("startDate"); + if (startDate != null && startDate.isJsonPrimitive()) { + try { + return startDate.getAsJsonPrimitive().getAsLong(); + } catch (NumberFormatException ignored) {} } + return null; + } + + private SummaryEvent getSummaryEventNoSync() { Long startDate = null; - for (String key : features.keySet()) { - JsonObject asJsonObject = features.get(key).getAsJsonObject(); - if (asJsonObject.has("startDate")) { - startDate = asJsonObject.get("startDate").getAsLong(); - asJsonObject.remove("startDate"); - break; + JsonObject features = new JsonObject(); + for (String key : sharedPreferences.getAll().keySet()) { + JsonObject featureSummary = getValueAsJsonObject(key); + if (featureSummary != null) { + Long featureStartDate = removeStartDateFromFeatureSummary(featureSummary); + if (featureStartDate != null) { + startDate = featureStartDate; + } + features.add(key, featureSummary); } } + + if (startDate == null) { + return null; + } + SummaryEvent summaryEvent = new SummaryEvent(startDate, System.currentTimeMillis(), features); Timber.d("Sending Summary Event: %s", summaryEvent.toString()); return summaryEvent; @@ -112,9 +128,7 @@ private SummaryEvent getSummaryEventNoSync() { @Override public synchronized SummaryEvent getSummaryEventAndClear() { SummaryEvent summaryEvent = getSummaryEventNoSync(); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.clear(); - editor.apply(); + clear(); return summaryEvent; } @@ -129,11 +143,10 @@ private JsonObject createNewEvent(JsonElement value, JsonElement defaultVal, int private void addNewCountersElement(JsonArray countersArray, @Nullable JsonElement value, int version, JsonElement variation) { JsonObject newCounter = new JsonObject(); + newCounter.add("value", value); if (version == -1) { newCounter.add("unknown", new JsonPrimitive(true)); - newCounter.add("value", value); } else { - newCounter.add("value", value); newCounter.add("version", new JsonPrimitive(version)); newCounter.add("variation", variation); } @@ -141,44 +154,27 @@ private void addNewCountersElement(JsonArray countersArray, @Nullable JsonElemen countersArray.add(newCounter); } - - private JsonObject getFeaturesJsonObject() { - JsonObject returnObject = new JsonObject(); - for (String key : sharedPreferences.getAll().keySet()) { - returnObject.add(key, getValueAsJsonObject(key)); - } - return returnObject; - } - @SuppressLint("ApplySharedPref") @Nullable private JsonObject getValueAsJsonObject(String flagResponseKey) { - String storedFlag; try { - storedFlag = sharedPreferences.getString(flagResponseKey, null); - } catch (ClassCastException castException) { - // An old version of shared preferences is stored, so clear it. - // The flag responses will get re-synced with the server - sharedPreferences.edit().clear().commit(); - return null; - } - - if (storedFlag == null) { - return null; - } - - JsonElement element = new JsonParser().parse(storedFlag); - if (element instanceof JsonObject) { - return (JsonObject) element; + String storedFlag = sharedPreferences.getString(flagResponseKey, null); + if (storedFlag == null) { + return null; + } + JsonElement element = new JsonParser().parse(storedFlag); + if (element instanceof JsonObject) { + return element.getAsJsonObject(); + } + } catch (ClassCastException | JsonParseException ignored) { + // Fallthrough to clear } - + // An old version of shared preferences is stored, so clear it. + sharedPreferences.edit().clear().commit(); return null; } public synchronized void clear() { - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.clear(); - editor.apply(); + sharedPreferences.edit().clear().apply(); } - } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Throttler.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Throttler.java index ce9f052d..32750920 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Throttler.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Throttler.java @@ -15,62 +15,65 @@ class Throttler { @NonNull - private final Runnable runnable; + private final Runnable taskRunnable; private final long maxRetryTimeMs; private final long retryTimeMs; - private final Random jitter; - private final AtomicInteger attempts; - private final AtomicBoolean maxAttemptsReached; + private final Random jitter = new Random(); + private final AtomicInteger attempts = new AtomicInteger(-1); + private final AtomicBoolean queuedRun = new AtomicBoolean(false); private final HandlerThread handlerThread; private final Handler handler; - private final Runnable attemptsResetRunnable; + private final Runnable resetRunnable; - Throttler(@NonNull Runnable runnable, long retryTimeMs, long maxRetryTimeMs) { - this.runnable = runnable; + Throttler(@NonNull final Runnable runnable, long retryTimeMs, long maxRetryTimeMs) { this.retryTimeMs = retryTimeMs; this.maxRetryTimeMs = maxRetryTimeMs; - jitter = new Random(); - attempts = new AtomicInteger(0); - maxAttemptsReached = new AtomicBoolean(false); handlerThread = new HandlerThread("LDThrottler"); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); - attemptsResetRunnable = new Runnable() { + taskRunnable = new Runnable() { @Override public void run() { - Throttler.this.runnable.run(); - attempts.set(0); - maxAttemptsReached.set(false); + queuedRun.set(false); + runnable.run(); + } + }; + resetRunnable = new Runnable() { + @Override + public void run() { + attempts.decrementAndGet(); } }; } void attemptRun() { - // First invocation is instant, as is the first invocation after throttling has ended - if (attempts.get() == 0) { - runnable.run(); - attempts.getAndIncrement(); + int attempt = attempts.getAndIncrement(); + + // Grace first run instant for client initialization + if (attempt < 0) { + taskRunnable.run(); return; } - long jitterVal = calculateJitterVal(attempts.getAndIncrement()); + // First invocation is instant, as is the first invocation after throttling has ended + if (attempt == 0) { + taskRunnable.run(); + handler.postDelayed(resetRunnable, retryTimeMs); + return; + } - // Once the max retry time is reached, just let it run out - if (!maxAttemptsReached.get()) { - if (jitterVal == maxRetryTimeMs) { - maxAttemptsReached.set(true); - } - long sleepTimeMs = backoffWithJitter(jitterVal); - handler.removeCallbacks(attemptsResetRunnable); - handler.postDelayed(attemptsResetRunnable, sleepTimeMs); + long jitterVal = calculateJitterVal(attempt); + handler.postDelayed(resetRunnable, jitterVal); + if (!queuedRun.getAndSet(true)) { + handler.postDelayed(taskRunnable, backoffWithJitter(jitterVal)); } } void cancel() { - handler.removeCallbacks(attemptsResetRunnable); + handler.removeCallbacks(taskRunnable); } long calculateJitterVal(int reconnectAttempts) { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ValueTypes.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ValueTypes.java index 2f0b1b86..4e04b34a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ValueTypes.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ValueTypes.java @@ -73,6 +73,19 @@ public JsonElement valueToJson(@NonNull Float value) { } }; + static final Converter DOUBLE = new Converter() { + @Override + public Double valueFromJson(@NonNull JsonElement jsonValue) { + return (jsonValue.isJsonPrimitive() && jsonValue.getAsJsonPrimitive().isNumber()) ? jsonValue.getAsDouble() : null; + } + + @NonNull + @Override + public JsonElement valueToJson(@NonNull Double value) { + return new JsonPrimitive(value); + } + }; + static final Converter STRING = new Converter() { @Override public String valueFromJson(@NonNull JsonElement jsonValue) {