diff --git a/.circleci/config.yml b/.circleci/config.yml index 01879571..a10c45b4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -47,7 +47,9 @@ jobs: timeout: 1200 no_output_timeout: 2h - - run: ./gradlew :launchdarkly-android-client-sdk:assembleDebug --console=plain -PdisablePreDex + - run: + name: Compile + command: ./gradlew :launchdarkly-android-client-sdk:assembleDebug --console=plain -PdisablePreDex - run: name: Wait for emulator to boot @@ -73,12 +75,18 @@ jobs: adb logcat >> ~/artifacts/logcat.txt - run: - name: Run Tests + name: Run tests command: ./gradlew :launchdarkly-android-client-sdk:connectedAndroidTest --console=plain -PdisablePreDex no_output_timeout: 2h - - run: ./gradlew packageRelease --console=plain -PdisablePreDex + - run: + name: Validate package creation + command: ./gradlew packageRelease --console=plain -PdisablePreDex + - run: + name: Validate Javadoc + command: ./gradlew Javadoc + - run: name: Save test results command: | diff --git a/CHANGELOG.md b/CHANGELOG.md index e614113a..184d1361 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.9.1] - 2020-01-03 +### Fixed: +- Removed possibility of fatal `SecurityException` on Samsung devices that would be triggered when the SDK attempted to register an alarm to trigger a future poll when the application process already had 500 alarms registered. This limit is only present on Samsung's versions of Android Lollipop and later. The SDK will now catch this error if it occurs to prevent killing the host application. +- Rarely, the client would deliver its initial "identify" event to LaunchDarkly immediately rather than waiting for the configured flush interval. +- Fixed some malformed Javadoc comments. + ## [2.9.0] - 2019-10-25 ### Added - Added support for new LaunchDarkly experimentation features. See `LDClient.track(String, JsonElement, Double)` for recording numeric metrics. diff --git a/example/build.gradle b/example/build.gradle index 69540ede..64dac3b4 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.9.0' + //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.9.1' 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 45b7a8ce..0f9eb19e 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.9.0' + version = '2.9.1' sourceCompatibility = 1.7 targetCompatibility = 1.7 } @@ -40,10 +40,6 @@ android { execution 'ANDROID_TEST_ORCHESTRATOR' } - configurations { - javadocDeps - } - compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 @@ -89,10 +85,6 @@ dependencies { androidTestImplementation 'org.easymock:easymock:3.6' androidTestImplementation 'junit:junit:4.12' androidTestImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" - - javadocDeps "com.google.code.gson:gson:$gsonVersion" - javadocDeps "com.squareup.okhttp3:okhttp:$okhttpVersion" - javadocDeps "com.launchdarkly:okhttp-eventsource:$eventsourceVersion" } repositories { @@ -125,10 +117,15 @@ task sourcesJar(type: Jar) { } task javadoc(type: Javadoc) { - failOnError false source = android.sourceSets.main.java.srcDirs classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) - classpath += configurations.javadocDeps +} + +afterEvaluate { +// https://stackoverflow.com/questions/34571371/android-studio-javadoc-cannot-find-symbol/34572606#34572606 + javadoc.classpath += files(android.libraryVariants.collect { variant -> + variant.javaCompile.classpath.files + }) } task javadocJar(type: Jar, dependsOn: javadoc) { 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 bf5e932d..f79a014b 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 @@ -28,7 +28,6 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static junit.framework.Assert.fail; @@ -38,6 +37,8 @@ @RunWith(AndroidJUnit4.class) public class LDClientTest { + private static final String mobileKey = "test-mobile-key"; + @Rule public final ActivityTestRule activityTestRule = new ActivityTestRule<>(TestActivity.class, false, true); @@ -191,277 +192,216 @@ public void run() { @Test public void testTrack() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - ldClient.track("test-event"); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertNull(event.data); - assertNull(event.metricValue); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + + // Don't wait as we are not set offline + ldClient = LDClient.init(application, ldConfig, ldUser, 0); + + ldClient.track("test-event"); + ldClient.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertNull(event.data); + assertNull(event.metricValue); + } } @Test public void testTrackData() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - JsonPrimitive testData = new JsonPrimitive("abc"); - - ldClient.track("test-event", testData); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertEquals(testData, event.data); - assertNull(event.metricValue); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + // Don't wait as we are not set offline + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + JsonPrimitive testData = new JsonPrimitive("abc"); + + client.track("test-event", testData); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertEquals(testData, event.data); + assertNull(event.metricValue); + } + } } @Test public void testTrackDataNull() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - ldClient.track("test-event", null); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertNull(event.data); - assertNull(event.metricValue); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.track("test-event", null); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertNull(event.data); + assertNull(event.metricValue); + } + } } @Test public void testTrackMetric() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - ldClient.track("test-event", null, 5.5); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertNull(event.data); - assertEquals(5.5, event.metricValue, 0); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.track("test-event", null, 5.5); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertNull(event.data); + assertEquals(5.5, event.metricValue, 0); + } + } } @Test public void testTrackMetricNull() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - ldClient.track("test-event", null, null); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertNull(event.data); - assertNull(event.metricValue); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.track("test-event", null, null); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertNull(event.data); + assertNull(event.metricValue); + } + } } @Test public void testTrackDataAndMetric() throws IOException, InterruptedException { - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey("test-mobile-sdk-key") - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - JsonObject testData = new JsonObject(); - testData.add("data", new JsonPrimitive(10)); - - ldClient.track("test-event", testData, -10.0); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(2, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof CustomEvent); - CustomEvent event = (CustomEvent) events[1]; - assertEquals("userKey", event.userKey); - assertEquals("test-event", event.key); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertEquals(testData, event.data); - assertEquals(-10.0, event.metricValue); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + JsonObject testData = new JsonObject(); + testData.add("data", new JsonPrimitive(10)); + + client.track("test-event", testData, -10.0); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 2); + assertEquals(2, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof CustomEvent); + CustomEvent event = (CustomEvent) events[1]; + assertEquals("userKey", event.userKey); + assertEquals("test-event", event.key); + assertEquals(testData, event.data); + assertEquals(-10.0, event.metricValue); + } + } } @Test public void variationFlagTrackReasonGeneratesEventWithReason() throws IOException, InterruptedException { - // Setup events server - MockWebServer mockEventsServer = new MockWebServer(); - mockEventsServer.start(); - // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); - - HttpUrl baseUrl = mockEventsServer.url("/mobile"); - - String mobileKey = "test-mobile-key"; - LDConfig ldConfig = new LDConfig.Builder() - .setMobileKey(mobileKey) - .setEventsUri(Uri.parse(baseUrl.url().toString())) - .build(); + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + + // Setup flag store with test flag + TestUtil.markMigrationComplete(application); + EvaluationReason testReason = EvaluationReason.off(); + FlagStore flagStore = new SharedPrefsFlagStoreFactory(application).createFlagStore(mobileKey + ldUser.getSharedPrefsKey()); + flagStore.applyFlagUpdate(new FlagBuilder("track-reason-flag").trackEvents(true).trackReason(true).reason(testReason).build()); + + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.boolVariation("track-reason-flag", false); + client.blockingFlush(); + + Event[] events = getEventsFromLastRequest(mockEventsServer, 3); + assertEquals(3, events.length); + assertTrue(events[0] instanceof IdentifyEvent); + assertTrue(events[1] instanceof FeatureRequestEvent); + FeatureRequestEvent event = (FeatureRequestEvent) events[1]; + assertEquals("track-reason-flag", event.key); + assertEquals("userKey", event.userKey); + assertNull(event.variation); + assertNull(event.version); + assertFalse(event.value.getAsBoolean()); + assertFalse(event.defaultVal.getAsBoolean()); + assertEquals(testReason, event.reason); + assertTrue(events[2] instanceof SummaryEvent); + } + } + } + + private Event[] getEventsFromLastRequest(MockWebServer server, int expectedCount) throws InterruptedException { + RecordedRequest r = server.takeRequest(); + assertEquals("POST", r.getMethod()); + assertEquals("/mobile", r.getPath()); + assertEquals(LDConfig.AUTH_SCHEME + mobileKey, r.getHeader("Authorization")); + String body = r.getBody().readUtf8(); + System.out.println(body); + Event[] events = TestUtil.getEventDeserializerGson().fromJson(body, Event[].class); + if (events.length != expectedCount) { + assertTrue("count should be " + expectedCount + " for: " + body, false); + } + return events; + } - // Setup flag store with test flag - TestUtil.markMigrationComplete(application); - EvaluationReason testReason = EvaluationReason.off(); - FlagStore flagStore = new SharedPrefsFlagStoreFactory(application).createFlagStore(mobileKey + ldUser.getSharedPrefsKey()); - flagStore.applyFlagUpdate(new FlagBuilder("track-reason-flag").trackEvents(true).trackReason(true).reason(testReason).build()); - - // Don't wait as we are not set offline - ldClient = LDClient.init(application, ldConfig, ldUser, 0); - - ldClient.boolVariation("track-reason-flag", false); - ldClient.blockingFlush(); - - RecordedRequest eventPost = mockEventsServer.takeRequest(); - assertEquals("POST", eventPost.getMethod()); - assertEquals("/mobile", eventPost.getPath()); - assertNotNull(eventPost.getHeader("Authorization")); - - Event[] events = TestUtil.getEventDeserializerGson().fromJson(eventPost.getBody().readUtf8(), Event[].class); - assertEquals(3, events.length); - assertTrue(events[0] instanceof IdentifyEvent); - assertTrue(events[1] instanceof FeatureRequestEvent); - FeatureRequestEvent event = (FeatureRequestEvent) events[1]; - assertEquals("track-reason-flag", event.key); - assertEquals("userKey", event.userKey); - assertNull(event.variation); - assertNull(event.version); - assertFalse(event.value.getAsBoolean()); - assertFalse(event.defaultVal.getAsBoolean()); - assertEquals(testReason, event.reason); - assertEquals(System.currentTimeMillis(), event.creationDate, 500); - assertTrue(events[2] instanceof SummaryEvent); + private LDConfig.Builder baseConfigBuilder(MockWebServer server) { + HttpUrl baseUrl = server.url("/mobile"); + return new LDConfig.Builder() + .setMobileKey(mobileKey) + .setEventsUri(Uri.parse(baseUrl.toString())); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultEventProcessor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultEventProcessor.java index ff4856e6..fb8af6e3 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultEventProcessor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultEventProcessor.java @@ -87,7 +87,7 @@ public Thread newThread(@NonNull Runnable r) { } }); - scheduler.scheduleAtFixedRate(consumer, 0, config.getEventsFlushIntervalMillis(), TimeUnit.MILLISECONDS); + scheduler.scheduleAtFixedRate(consumer, config.getEventsFlushIntervalMillis(), config.getEventsFlushIntervalMillis(), TimeUnit.MILLISECONDS); } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Foreground.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Foreground.java index a3302190..971f4b5d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Foreground.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Foreground.java @@ -20,16 +20,16 @@ /** * Usage: - *

+ *

* 1. Get the Foreground Singleton, passing a Context or Application object unless you * are sure that the Singleton has definitely already been initialised elsewhere. - *

+ *

* 2.a) Perform a direct, synchronous check: Foreground.isForeground() / .isBackground() - *

+ *

* or - *

+ *

* 2.b) Register to be notified (useful in Service or other non-UI components): - *

+ *

* Foreground.Listener myListener = new Foreground.Listener(){ * void onBecameForeground(){ * // ... whatever you want to do @@ -38,12 +38,12 @@ * // ... whatever you want to do * } * } - *

+ *

* void onCreate(){ * super.onCreate(); * Foreground.get(this).addListener(listener); * } - *

+ *

* void onDestroy(){ * super.onCreate(); * Foreground.get(this).removeListener(listener); 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 ccfdf5ab..c89c5ff4 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 @@ -58,7 +58,7 @@ public class LDClient implements LDClientInterface, Closeable { * will complete once the client has been initialized with the latest feature flag values. For * immediate access to the Client (possibly with out of date feature flags), it is safe to ignore * the return value of this method, and afterward call {@link #get()} - *

+ *

* If the client has already been initialized, is configured for offline mode, or the device is * not connected to the internet, this method will return a {@link Future} that is * already in the completed state. @@ -401,7 +401,7 @@ private EvaluationDetail variationDetailInternal(String flagKey, T fallba /** * Closes the client. This should only be called at the end of a client's lifecycle. * - * @throws IOException + * @throws IOException declared by the Closeable interface, but will not be thrown by the client */ @Override public void close() throws IOException { 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 67e91c25..4fe73fec 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 @@ -37,7 +37,7 @@ public interface LDClientInterface extends Closeable { * Shuts down any network connections maintained by the client and puts the client in offline * mode, preventing the client from opening new network connections until * setOnline() is called. - *

+ *

* Note: The client automatically monitors the device's network connectivity and app foreground * status, so calling setOffline() or setOnline() is normally * unnecessary in most situations. @@ -47,7 +47,7 @@ public interface LDClientInterface extends Closeable { /** * Restores network connectivity for the client, if the client was previously in offline mode. * This operation may be throttled if it is called too frequently. - *

+ *

* Note: The client automatically monitors the device's network connectivity and app foreground * status, so calling setOffline() or setOnline() is normally * unnecessary in most situations. @@ -274,14 +274,14 @@ public interface LDClientInterface extends Closeable { ConnectionInformation getConnectionInformation(); /** - * - * @param LDStatusListener + * Unregisters a {@link LDStatusListener} so it will no longer be called on connection status updates. + * @param LDStatusListener the listener to be removed */ void unregisterStatusListener(LDStatusListener LDStatusListener); /** - * - * @param LDStatusListener + * Registers a {@link LDStatusListener} to be called on connection status updates. + * @param LDStatusListener the listener to be called on a connection status update */ void registerStatusListener(LDStatusListener LDStatusListener); 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 54539599..64af036a 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 @@ -418,6 +418,7 @@ public LDConfig.Builder setPollingIntervalMillis(int pollingIntervalMillis) { * * @param backgroundPollingIntervalMillis the feature flag polling interval when in the background, * in milliseconds + * @return the builder */ public LDConfig.Builder setBackgroundPollingIntervalMillis(int backgroundPollingIntervalMillis) { this.backgroundPollingIntervalMillis = backgroundPollingIntervalMillis; @@ -430,6 +431,7 @@ public LDConfig.Builder setBackgroundPollingIntervalMillis(int backgroundPolling * The default value is false (flag updates will be done in the background). * * @param disableBackgroundUpdating true if the client should skip updating flags when in the background + * @return the builder */ public LDConfig.Builder setDisableBackgroundUpdating(boolean disableBackgroundUpdating) { this.disableBackgroundUpdating = disableBackgroundUpdating; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDCountryCode.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDCountryCode.java index e1f94a52..caefea1b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDCountryCode.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDCountryCode.java @@ -33,7 +33,6 @@ /** * ISO 3166-1 country code. - *

*

* Enum names of this enum themselves are represented by * ISO 3166-1 alpha-2 @@ -47,7 +46,6 @@ * corresponds to a given alpha-2/alpha-3/numeric code ({@link #getByCode(String)}, * {@link #getByCode(int)}). *

- *

*

  * // List all the country codes.
  * for (CountryCode code : CountryCode.values())
@@ -55,10 +53,10 @@
  * // For example, "[US] United States" is printed.
  * System.out.format("[%s] %s\n", code, code.{@link #getName()});
  * }
- * 

+ * * // Get a CountryCode instance by ISO 3166-1 code. * CountryCode code = CountryCode.{@link #getByCode(String) getByCode}("JP"); - *

+ * * // Print all the information. Output will be: * // * // Country name = Japan @@ -72,16 +70,16 @@ * System.out.println("ISO 3166-1 alpha-3 code = " + code.{@link #getAlpha3()}); * System.out.println("ISO 3166-1 numeric code = " + code.{@link #getNumeric()}); * System.out.println("Assignment state = " + code.{@link #getAssignment()}); - *

+ * * // Convert to a Locale instance. * {@link Locale} locale = code.{@link #toLocale()}; - *

+ * * // Get a CountryCode by a Locale instance. * code = CountryCode.{@link #getByLocale(Locale) getByLocale}(locale); - *

+ * * // Get the currency of the country. * {@link Currency} currency = code.{@link #getCurrency()}; - *

+ * * // Get a list by a regular expression for names. * // * // The list will contain: @@ -1818,7 +1816,6 @@ public Locale toLocale() { * East Timor * [TP, TPTL, 0, * Traditionally reserved] - *

*

* ISO 3166-1 numeric code is unknown. *

@@ -2033,7 +2030,6 @@ public Locale toLocale() { * Zaire * [ZR, ZRCD, 0, * Traditionally reserved] - *

*

* ISO 3166-1 numeric code is unknown. *

@@ -2059,7 +2055,7 @@ enum Assignment { /** * Officially assigned. - *

+ *

* Assigned to a country, territory, or area of geographical interest. */ OFFICIALLY_ASSIGNED, @@ -2067,7 +2063,7 @@ enum Assignment { /** * User assigned. - *

+ *

* Free for assignment at the disposal of users. */ USER_ASSIGNED, @@ -2075,7 +2071,7 @@ enum Assignment { /** * Exceptionally reserved. - *

+ *

* Reserved on request for restricted use. */ EXCEPTIONALLY_RESERVED, @@ -2083,7 +2079,7 @@ enum Assignment { /** * Transitionally reserved. - *

+ *

* Deleted from ISO 3166-1 but reserved transitionally. */ TRANSITIONALLY_RESERVED, @@ -2091,7 +2087,7 @@ enum Assignment { /** * Indeterminately reserved. - *

+ *

* Used in coding systems associated with ISO 3166-1. */ INDETERMINATELY_RESERVED, @@ -2099,7 +2095,7 @@ enum Assignment { /** * Not used. - *

+ *

* Not used in ISO 3166-1 in deference to international property * organization names. */ @@ -2202,22 +2198,19 @@ public Assignment getAssignment() { /** * Convert this {@code CountryCode} instance to a {@link Locale} instance. - *

*

* In most cases, this method creates a new {@code Locale} instance * every time it is called, but some {@code CountryCode} instances return * their corresponding entries in {@code Locale} class. For example, * {@link #CA CountryCode.CA} always returns {@link Locale#CANADA}. *

- *

*

* The table below lists {@code CountryCode} entries whose {@code toLocale()} * do not create new Locale instances but return entries in * {@code Locale} class. *

- *

* - * + * * * * @@ -2272,7 +2265,6 @@ public Locale toLocale() { /** * Get the currency. - *

*

* This method is an alias of {@link Currency}{@code .}{@link * Currency#getInstance(Locale) getInstance}{@code (}{@link @@ -2280,13 +2272,11 @@ public Locale toLocale() { * returns {@code null} when {@code Currency.getInstance(Locale)} * throws {@code IllegalArgumentException}. *

- *

*

* This method returns {@code null} when the territory represented by * this {@code CountryCode} instance does not have a currency. * {@link #AQ} (Antarctica) is one example. *

- *

*

* In addition, this method returns {@code null} also when the ISO 3166 * code represented by this {@code CountryCode} instance is not @@ -2315,7 +2305,6 @@ public Currency getCurrency() { * Get a {@code CountryCode} that corresponds to the given ISO 3166-1 * alpha-2 or * alpha-3 code. - *

*

* This method calls {@link #getByCode(String, boolean) getByCode}{@code (code, true)}. * Note that the behavior has changed since the version 1.13. In the older versions, @@ -2337,7 +2326,6 @@ public static LDCountryCode getByCode(String code) { * Get a {@code CountryCode} that corresponds to the given ISO 3166-1 * alpha-2 or * alpha-3 code. - *

*

* This method calls {@link #getByCode(String, boolean) getByCode}{@code (code, false)}. *

@@ -2467,7 +2455,6 @@ public static LDCountryCode getByCode(int code) { /** * Get a list of {@code CountryCode} by a name regular expression. - *

*

* This method is almost equivalent to {@link #findByName(Pattern) * findByName}{@code (Pattern.compile(regex))}. @@ -2495,19 +2482,15 @@ public static List findByName(String regex) { /** * Get a list of {@code CountryCode} by a name pattern. - *

*

* For example, the list obtained by the code snippet below: *

- *

*

      * Pattern pattern = Pattern.compile(".*United.*");
      * List<CountryCode> list = CountryCode.findByName(pattern);
- *

*

* contains 6 {@code CountryCode}s as listed below. *

- *

*

    *
  1. {@link #AE} : United Arab Emirates *
  2. {@link #GB} : United Kingdom diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDUser.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDUser.java index 599e9a3a..80549e1c 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDUser.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDUser.java @@ -29,12 +29,12 @@ * mandatory property property is the {@code key}, which must uniquely identify each user. For * authenticated users, this may be a username or e-mail address. For anonymous users, this could be * an IP address or session ID. - *

    + *

    * Besides the mandatory {@code key}, {@code LDUser} supports two kinds of optional attributes: * interpreted attributes (e.g. {@code ip} and {@code country}) and custom attributes. LaunchDarkly * can parse interpreted attributes and attach meaning to them. For example, from an {@code ip} * address, LaunchDarkly can do a geo IP lookup and determine the user's country. - *

    + *

    * Custom attributes are not parsed by LaunchDarkly. They can be used in custom rules-- for example, * a custom attribute such as "customer_ranking" can be used to launch a feature to the top 10% of * users on a site. @@ -192,7 +192,6 @@ String getSharedPrefsKey() { /** * A builder that helps construct * {@link LDUser} objects. Builder calls can be chained, enabling the following pattern: - *

    *

          * LDUser user = new LDUser.Builder("key")
          *      .country("US")
    diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/PollingUpdater.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/PollingUpdater.java
    index 37e0c1ca..9a0e1405 100644
    --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/PollingUpdater.java
    +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/PollingUpdater.java
    @@ -33,11 +33,15 @@ synchronized static void startPolling(Context context, int initialDelayMillis, i
             PendingIntent pendingIntent = getPendingIntent(context);
             AlarmManager alarmMgr = getAlarmManager(context);
     
    -        alarmMgr.setInexactRepeating(
    -                AlarmManager.ELAPSED_REALTIME,
    -                SystemClock.elapsedRealtime() + initialDelayMillis,
    -                intervalMillis,
    -                pendingIntent);
    +        try {
    +            alarmMgr.setInexactRepeating(
    +                    AlarmManager.ELAPSED_REALTIME,
    +                    SystemClock.elapsedRealtime() + initialDelayMillis,
    +                    intervalMillis,
    +                    pendingIntent);
    +        } catch (SecurityException ex) {
    +            Timber.w(ex, "SecurityException when setting background polling alarm");
    +        }
         }
     
         synchronized static void stop(Context context) {
    diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SummaryEventSharedPreferences.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SummaryEventSharedPreferences.java
    index f41d0cf2..0947f257 100644
    --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SummaryEventSharedPreferences.java
    +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SummaryEventSharedPreferences.java
    @@ -5,9 +5,8 @@
     import com.google.gson.JsonElement;
     
     /**
    - * Created by jamesthacker on 4/12/18.
    + * Used internally by the SDK.
      */
    -
     public interface SummaryEventSharedPreferences {
     
         void clear();
    diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/FlagsResponse.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/FlagsResponse.java
    index 034fe823..7e677470 100644
    --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/FlagsResponse.java
    +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/FlagsResponse.java
    @@ -7,9 +7,7 @@
     import java.util.List;
     
     /**
    - * Used for cases where the server sends a collection of flags as a key-value object. Uses custom
    - * deserializer in {@link com.launchdarkly.android.gson.FlagsResponseSerialization} to get a list of
    - * {@link com.launchdarkly.android.flagstore.Flag} objects.
    + * Used for cases where the server sends a collection of flags as a key-value object.
      */
     public class FlagsResponse {
         @NonNull
    
CountryCodeLocale