diff --git a/CHANGELOG.md b/CHANGELOG.md index 184d1361..b667efca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ All notable changes to the LaunchDarkly Android SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.10.0] - 2020-01-30 +### Added +- The SDK now specifies a uniquely identifiable request header when sending events to LaunchDarkly to ensure that events are only processed once, even if the SDK sends them two times due to a failed initial attempt. +### Deprecated +- All classes in sub-packages, which were only intended for use by the SDK. These classes will be removed in the next major release. +- `LDCountryCode`, as well as `LDUser` setters that took `LDCountryCode` as an argument. The `String` overloads should be used instead, as these will be removed in the next major release. Until that release the additional validation on the country fields will remain, see the Javadoc for more information. + ## [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. diff --git a/example/build.gradle b/example/build.gradle index 64dac3b4..977eaba6 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.1' + //implementation 'com.launchdarkly:launchdarkly-android-client-sdk:2.10.-' 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 0f9eb19e..e4724c25 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.1' + version = '2.10.0' sourceCompatibility = 1.7 targetCompatibility = 1.7 } diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DebounceTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DebounceTest.java index fcb6d675..48aafd64 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DebounceTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DebounceTest.java @@ -9,9 +9,6 @@ import static org.junit.Assert.assertEquals; -/** - * Created by pwray on 2017-09-27. - */ @RunWith(AndroidJUnit4.class) public class DebounceTest { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DefaultUserManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DefaultUserManagerTest.java index 4a8cbe4e..598e2ab9 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DefaultUserManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DefaultUserManagerTest.java @@ -4,8 +4,6 @@ import android.support.test.runner.AndroidJUnit4; import com.google.gson.JsonObject; -import com.launchdarkly.android.flagstore.Flag; -import com.launchdarkly.android.flagstore.FlagStore; import com.launchdarkly.android.test.TestActivity; import org.easymock.Capture; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DeleteFlagResponseTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DeleteFlagResponseTest.java index 2ab61f1c..061871e2 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DeleteFlagResponseTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/DeleteFlagResponseTest.java @@ -3,10 +3,6 @@ import android.support.test.runner.AndroidJUnit4; import com.google.gson.Gson; -import com.launchdarkly.android.flagstore.Flag; -import com.launchdarkly.android.flagstore.FlagBuilder; -import com.launchdarkly.android.gson.GsonCache; -import com.launchdarkly.android.response.DeleteFlagResponse; import org.junit.Rule; import org.junit.Test; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/EventTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/EventTest.java index ac3e4a2c..cf962bdd 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/EventTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/EventTest.java @@ -20,9 +20,6 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -/** - * Created by Farhan on 2018-01-04. - */ @RunWith(AndroidJUnit4.class) public class EventTest { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagBuilder.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagBuilder.java similarity index 94% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagBuilder.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagBuilder.java index 4f46b6b5..8cba3916 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagBuilder.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagBuilder.java @@ -1,9 +1,8 @@ -package com.launchdarkly.android.flagstore; +package com.launchdarkly.android; import android.support.annotation.NonNull; import com.google.gson.JsonElement; -import com.launchdarkly.android.EvaluationReason; public class FlagBuilder { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreManagerTest.java similarity index 99% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreManagerTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreManagerTest.java index 337858df..e4c49358 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreManagerTest.java @@ -1,11 +1,9 @@ -package com.launchdarkly.android.flagstore; +package com.launchdarkly.android; import android.os.Looper; import android.support.annotation.NonNull; import android.util.Pair; -import com.launchdarkly.android.FeatureFlagChangeListener; - import org.easymock.Capture; import org.easymock.EasyMockSupport; import org.easymock.IAnswer; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreTest.java similarity index 99% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreTest.java index 536c1c3c..6992314c 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagStoreTest.java @@ -1,11 +1,10 @@ -package com.launchdarkly.android.flagstore; +package com.launchdarkly.android; import android.util.Pair; import com.google.gson.JsonArray; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import com.launchdarkly.android.EvaluationReason; import org.easymock.EasyMockSupport; import org.easymock.IArgumentMatcher; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagTest.java similarity index 98% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagTest.java index 7cb84f06..f4359448 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/FlagTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/FlagTest.java @@ -1,13 +1,10 @@ -package com.launchdarkly.android.flagstore; +package com.launchdarkly.android; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import com.launchdarkly.android.EvaluationReason; -import com.launchdarkly.android.TimberLoggingRule; -import com.launchdarkly.android.gson.GsonCache; import org.junit.Before; import org.junit.Rule; 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 f79a014b..e431ac4d 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 @@ -7,9 +7,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -import com.launchdarkly.android.flagstore.FlagBuilder; -import com.launchdarkly.android.flagstore.FlagStore; -import com.launchdarkly.android.flagstore.sharedprefs.SharedPrefsFlagStoreFactory; import com.launchdarkly.android.test.TestActivity; import org.junit.Before; @@ -18,8 +15,10 @@ import org.junit.runner.RunWith; import java.io.IOException; +import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import okhttp3.HttpUrl; import okhttp3.mockwebserver.MockResponse; @@ -28,6 +27,7 @@ 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; @@ -195,7 +195,7 @@ public void testTrack() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); @@ -222,7 +222,7 @@ public void testTrackData() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); // Don't wait as we are not set offline @@ -250,7 +250,7 @@ public void testTrackDataNull() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { @@ -275,7 +275,7 @@ public void testTrackMetric() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { @@ -300,7 +300,7 @@ public void testTrackMetricNull() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { @@ -325,7 +325,7 @@ public void testTrackDataAndMetric() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { @@ -348,12 +348,71 @@ public void testTrackDataAndMetric() throws IOException, InterruptedException { } } + @Test + public void eventIncludesPayloadId() throws IOException, InterruptedException { + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse()); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.blockingFlush(); + } + + RecordedRequest r = mockEventsServer.takeRequest(); + String headerVal = r.getHeader("X-LaunchDarkly-Payload-ID"); + assertNotNull(headerVal); + // Throws if invalid UUID + assertNotNull(UUID.fromString(headerVal)); + } + } + + @Test + public void eventPayloadIdDiffersBetweenRequests() throws IOException, InterruptedException { + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a successful empty response + mockEventsServer.enqueue(new MockResponse()); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.blockingFlush(); + client.identify(ldUser); + client.blockingFlush(); + } + + String firstPayloadId = mockEventsServer.takeRequest().getHeader("X-LaunchDarkly-Payload-ID"); + String secondPayloadId = mockEventsServer.takeRequest().getHeader("X-LaunchDarkly-Payload-ID"); + assertFalse(firstPayloadId.equals(secondPayloadId)); + } + } + + @Test + public void eventPayloadIdSameOnRetry() throws IOException, InterruptedException { + try (MockWebServer mockEventsServer = new MockWebServer()) { + mockEventsServer.start(); + // Enqueue a failure followed by successful response + mockEventsServer.enqueue(new MockResponse().setResponseCode(429)); + mockEventsServer.enqueue(new MockResponse()); + + LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); + try (LDClient client = LDClient.init(application, ldConfig, ldUser, 0)) { + client.blockingFlush(); + } + + String initialPayloadId = mockEventsServer.takeRequest(0, TimeUnit.SECONDS).getHeader("X-LaunchDarkly-Payload-ID"); + String retryPayloadId = mockEventsServer.takeRequest(0, TimeUnit.SECONDS).getHeader("X-LaunchDarkly-Payload-ID"); + assertTrue(initialPayloadId.equals(retryPayloadId)); + } + } + @Test public void variationFlagTrackReasonGeneratesEventWithReason() throws IOException, InterruptedException { try (MockWebServer mockEventsServer = new MockWebServer()) { mockEventsServer.start(); // Enqueue a successful empty response - mockEventsServer.enqueue(new MockResponse().addHeader("Date", "")); + mockEventsServer.enqueue(new MockResponse()); LDConfig ldConfig = baseConfigBuilder(mockEventsServer).build(); diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDUserTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDUserTest.java index 9e86c890..d76b4668 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDUserTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/LDUserTest.java @@ -20,9 +20,6 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -/** - * Created by Farhan on 2018-01-02. - */ @RunWith(AndroidJUnit4.class) public class LDUserTest { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactoryTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreFactoryTest.java similarity index 84% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactoryTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreFactoryTest.java index 925e81cf..7faf759a 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactoryTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreFactoryTest.java @@ -1,11 +1,13 @@ -package com.launchdarkly.android.flagstore.sharedprefs; +package com.launchdarkly.android; import android.app.Application; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; import com.launchdarkly.android.TimberLoggingRule; -import com.launchdarkly.android.flagstore.FlagStore; +import com.launchdarkly.android.FlagStore; +import com.launchdarkly.android.SharedPrefsFlagStore; +import com.launchdarkly.android.SharedPrefsFlagStoreFactory; import com.launchdarkly.android.test.TestActivity; import org.junit.Rule; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManagerTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreManagerTest.java similarity index 77% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManagerTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreManagerTest.java index ad233286..311d9229 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManagerTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreManagerTest.java @@ -1,13 +1,9 @@ -package com.launchdarkly.android.flagstore.sharedprefs; +package com.launchdarkly.android; import android.app.Application; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; -import com.launchdarkly.android.TimberLoggingRule; -import com.launchdarkly.android.flagstore.FlagStoreFactory; -import com.launchdarkly.android.flagstore.FlagStoreManager; -import com.launchdarkly.android.flagstore.FlagStoreManagerTest; import com.launchdarkly.android.test.TestActivity; import org.junit.Before; diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreTest.java similarity index 89% rename from launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreTest.java rename to launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreTest.java index 252d935d..135ab86f 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/SharedPrefsFlagStoreTest.java @@ -1,16 +1,9 @@ -package com.launchdarkly.android.flagstore.sharedprefs; +package com.launchdarkly.android; import android.app.Application; import android.support.test.rule.ActivityTestRule; import android.support.test.runner.AndroidJUnit4; -import com.launchdarkly.android.TimberLoggingRule; -import com.launchdarkly.android.flagstore.Flag; -import com.launchdarkly.android.flagstore.FlagBuilder; -import com.launchdarkly.android.flagstore.FlagStore; -import com.launchdarkly.android.flagstore.FlagStoreTest; -import com.launchdarkly.android.flagstore.FlagUpdate; -import com.launchdarkly.android.response.DeleteFlagResponse; import com.launchdarkly.android.test.TestActivity; import org.junit.Assert; 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 0b482025..64411ae4 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 @@ -12,10 +12,6 @@ import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; -/** - * Created by jamesthacker on 4/2/18. - */ - @RunWith(AndroidJUnit4.class) public class ThrottlerTest { diff --git a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/UserSummaryEventSharedPreferencesTest.java b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/UserSummaryEventSharedPreferencesTest.java index 1fa4a8cb..a61bb458 100644 --- a/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/UserSummaryEventSharedPreferencesTest.java +++ b/launchdarkly-android-client-sdk/src/androidTest/java/com/launchdarkly/android/UserSummaryEventSharedPreferencesTest.java @@ -18,10 +18,6 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; -/** - * Created by jamesthacker on 4/16/18. - */ - @RunWith(AndroidJUnit4.class) public class UserSummaryEventSharedPreferencesTest { 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 b423d2bb..59491f7f 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 @@ -5,8 +5,6 @@ import android.content.SharedPreferences; import android.support.annotation.NonNull; -import com.launchdarkly.android.gson.GsonCache; - import java.util.Calendar; import java.util.TimeZone; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityReceiver.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityReceiver.java index 8cb089a6..4ff3aa00 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityReceiver.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/ConnectivityReceiver.java @@ -6,6 +6,9 @@ import static com.launchdarkly.android.Util.isInternetConnected; +/** + * Used internally by the SDK. + */ public class ConnectivityReceiver extends BroadcastReceiver { static final String CONNECTIVITY_CHANGE = "android.net.conn.CONNECTIVITY_CHANGE"; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Debounce.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Debounce.java index 2649400f..2c59ec96 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Debounce.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Debounce.java @@ -5,7 +5,7 @@ import java.util.concurrent.Executors; /** - * Created by jkodumal on 9/18/17. + * Used internally by the SDK. */ public class Debounce { private volatile Callable pending; 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 fb8af6e3..0a9d44fb 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 @@ -5,10 +5,6 @@ import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; -import com.launchdarkly.android.tls.ModernTLSSocketFactory; -import com.launchdarkly.android.tls.SSLHandshakeInterceptor; -import com.launchdarkly.android.tls.TLSUtils; - import java.io.Closeable; import java.io.IOException; import java.security.GeneralSecurityException; @@ -18,6 +14,7 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.UUID; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; import java.util.concurrent.Executors; @@ -35,6 +32,7 @@ import static com.launchdarkly.android.LDConfig.JSON; import static com.launchdarkly.android.Util.isClientConnected; +import static com.launchdarkly.android.Util.isHttpErrorRecoverable; class DefaultEventProcessor implements EventProcessor, Closeable { private final BlockingQueue queue; @@ -151,24 +149,55 @@ synchronized void flush() { private void postEvents(List events) { String content = config.getFilteredEventGson().toJson(events); - Request request = config.getRequestBuilderFor(environmentName) - .url(config.getEventsUri().toString()) - .post(RequestBody.create(JSON, content)) - .addHeader("Content-Type", "application/json") - .addHeader("X-LaunchDarkly-Event-Schema", "3") - .build(); + String eventPayloadId = UUID.randomUUID().toString(); + String url = config.getEventsUri().toString(); - Timber.d("Posting %s event(s) to %s", events.size(), request.url()); + Timber.d("Posting %s event(s) to %s", events.size(), url); Timber.d("Events body: %s", content); - Response response = null; - try { - response = client.newCall(request).execute(); - Timber.d("Events Response: %s", response.code()); - Timber.d("Events Response Date: %s", response.header("Date")); + for (int attempt = 0; attempt < 2; attempt++) { + if (attempt > 0) { + Timber.w("Will retry posting events after 1 second"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) {} + } + + Request request = config.getRequestBuilderFor(environmentName) + .url(url) + .post(RequestBody.create(JSON, content)) + .addHeader("Content-Type", "application/json") + .addHeader("X-LaunchDarkly-Event-Schema", "3") + .addHeader("X-LaunchDarkly-Payload-ID", eventPayloadId) + .build(); + + Response response = null; + try { + response = client.newCall(request).execute(); + Timber.d("Events Response: %s", response.code()); + Timber.d("Events Response Date: %s", response.header("Date")); + + if (!response.isSuccessful()) { + Timber.w("Unexpected response status when posting events: %d", response.code()); + if (isHttpErrorRecoverable(response.code())) { + continue; + } + } + + tryUpdateDate(response); + break; + } catch (IOException e) { + Timber.e(e, "Unhandled exception in LaunchDarkly client attempting to connect to URI: %s", request.url()); + } finally { + if (response != null) response.close(); + } + } + } - String dateString = response.header("Date"); + private void tryUpdateDate(Response response) { + String dateString = response.header("Date"); + if (dateString != null) { SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); try { Date date = sdf.parse(dateString); @@ -176,10 +205,6 @@ private void postEvents(List events) { } catch (ParseException pe) { Timber.e(pe, "Failed to parse date header"); } - } catch (IOException e) { - Timber.e(e, "Unhandled exception in LaunchDarkly client attempting to connect to URI: %s", request.url()); - } finally { - if (response != null) response.close(); } } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultUserManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultUserManager.java index 405b0673..2ba30687 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultUserManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DefaultUserManager.java @@ -7,14 +7,6 @@ import android.util.Base64; import com.google.gson.JsonObject; -import com.launchdarkly.android.flagstore.Flag; -import com.launchdarkly.android.flagstore.FlagStore; -import com.launchdarkly.android.flagstore.FlagStoreManager; -import com.launchdarkly.android.flagstore.sharedprefs.SharedPrefsFlagStoreFactory; -import com.launchdarkly.android.flagstore.sharedprefs.SharedPrefsFlagStoreManager; -import com.launchdarkly.android.gson.GsonCache; -import com.launchdarkly.android.response.DeleteFlagResponse; -import com.launchdarkly.android.response.FlagsResponse; import java.util.Collection; import java.util.List; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DeleteFlagResponse.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DeleteFlagResponse.java new file mode 100644 index 00000000..b12a7a14 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/DeleteFlagResponse.java @@ -0,0 +1,32 @@ +package com.launchdarkly.android; + +class DeleteFlagResponse implements FlagUpdate { + + private final String key; + private final Integer version; + + DeleteFlagResponse(String key, Integer version) { + this.key = key; + this.version = version; + } + + /** + * Returns null to signal deletion of the flag if this update is valid on the supplied flag, + * otherwise returns the existing flag. + * + * @param before An existing Flag associated with flagKey from flagToUpdate() + * @return null, or the before flag. + */ + @Override + public Flag updateFlag(Flag before) { + if (before == null || version == null || before.isVersionMissing() || version > before.getVersion()) { + return null; + } + return before; + } + + @Override + public String flagToUpdate() { + return key; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Flag.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Flag.java new file mode 100644 index 00000000..33a594d3 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Flag.java @@ -0,0 +1,96 @@ +package com.launchdarkly.android; + +import android.support.annotation.NonNull; + +import com.google.gson.JsonElement; + +class Flag implements FlagUpdate, FlagInterface { + + @NonNull + private final String key; + private final JsonElement value; + private final Integer version; + private final Integer flagVersion; + private final Integer variation; + private final Boolean trackEvents; + private final Boolean trackReason; + private final Long debugEventsUntilDate; + private final EvaluationReason reason; + + @Deprecated + public Flag(@NonNull String key, JsonElement value, Integer version, Integer flagVersion, Integer variation, Boolean trackEvents, Long debugEventsUntilDate, EvaluationReason reason) { + this(key, value, version, flagVersion, variation, trackEvents, null, debugEventsUntilDate, reason); + } + + public Flag(@NonNull String key, JsonElement value, Integer version, Integer flagVersion, Integer variation, Boolean trackEvents, Boolean trackReason, Long debugEventsUntilDate, EvaluationReason reason) { + this.key = key; + this.value = value; + this.version = version; + this.flagVersion = flagVersion; + this.variation = variation; + this.trackEvents = trackEvents; + this.trackReason = trackReason; + this.debugEventsUntilDate = debugEventsUntilDate; + this.reason = reason; + } + + @NonNull + public String getKey() { + return key; + } + + public JsonElement getValue() { + return value; + } + + public Integer getVersion() { + return version; + } + + public Integer getFlagVersion() { + return flagVersion; + } + + public Integer getVariation() { + return variation; + } + + public boolean getTrackEvents() { + return trackEvents == null ? false : trackEvents; + } + + public boolean isTrackReason() { return trackReason == null ? false : trackReason; } + + public Long getDebugEventsUntilDate() { + return debugEventsUntilDate; + } + + @Override + public EvaluationReason getReason() { + return reason; + } + + public boolean isVersionMissing() { + return version == null; + } + + public int getVersionForEvents() { + if (flagVersion == null) { + return version == null ? -1 : version; + } + return flagVersion; + } + + @Override + public Flag updateFlag(Flag before) { + if (before == null || this.isVersionMissing() || before.isVersionMissing() || this.getVersion() > before.getVersion()) { + return this; + } + return before; + } + + @Override + public String flagToUpdate() { + return key; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagInterface.java new file mode 100644 index 00000000..265181bf --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagInterface.java @@ -0,0 +1,63 @@ +package com.launchdarkly.android; + +import android.support.annotation.NonNull; + +import com.google.gson.JsonElement; +import com.launchdarkly.android.EvaluationReason; + +/** + * Public interface for a Flag, to be used if exposing Flag model to public API methods. + */ +interface FlagInterface { + + /** + * Getter for flag's key + * + * @return The flag's key + */ + @NonNull + String getKey(); + + /** + * Getter for flag's value. The value along with the variation are provided by LaunchDarkly by + * evaluating full flag rules against the specific user. + * + * @return The flag's value + */ + JsonElement getValue(); + + /** + * Getter for the flag's environment version field. This is an environment global version that + * is updated whenever any flag is updated in an environment. This field is nullable, as + * LaunchDarkly may provide only one of version and flagVersion. + * + * @return The environment version for this flag + */ + Integer getVersion(); + + /** + * Getter for the flag's version. This is a flag specific version that is updated when the + * specific flag has been updated. This field is nullable, as LaunchDarkly may provide only one + * of version and flagVersion. + * + * @return The flag's version + */ + Integer getFlagVersion(); + + /** + * Getter for flag's variation. The variation along with the value are provided by LaunchDarkly + * by evaluating full flag rules against the specific user. + * + * @return The flag's variation + */ + Integer getVariation(); + + /** + * Getter for the flag's evaluation reason. The evaluation reason is provided by the server to + * describe the underlying conditions leading to the selection of the flag's variation and value + * when evaluated against the particular user. + * + * @return The reason describing the flag's evaluation result + */ + EvaluationReason getReason(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStore.java new file mode 100644 index 00000000..189787fb --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStore.java @@ -0,0 +1,86 @@ +package com.launchdarkly.android; + +import android.support.annotation.Nullable; + +import java.util.Collection; +import java.util.List; + +/** + * A FlagStore supports getting individual or collections of flag updates and updating an underlying + * persistent store. Individual flags can be retrieved by a flagKey, or all flags retrieved. Allows + * replacing backing store for flags at a future date, as well as mocking for unit testing. + */ +interface FlagStore { + + /** + * Delete the backing persistent store for this identifier entirely. Further operations on a + * FlagStore are undefined after calling this method. + */ + void delete(); + + /** + * Remove all flags from the store. + */ + void clear(); + + /** + * Returns true if a flag with the key is in the store, otherwise false. + * + * @param key The key to check for membership in the store. + * @return Whether a flag with the given key is in the store. + */ + boolean containsKey(String key); + + /** + * Get an individual flag from the store. If a flag with the key flagKey is not stored, returns + * null. + * + * @param flagKey The key to get the corresponding flag for. + * @return The flag with the key flagKey or null. + */ + @Nullable + Flag getFlag(String flagKey); + + /** + * Apply an individual flag update to the FlagStore. + * + * @param flagUpdate The FlagUpdate to apply. + */ + void applyFlagUpdate(FlagUpdate flagUpdate); + + /** + * Apply a list of flag updates to the FlagStore. + * + * @param flagUpdates The list of FlagUpdates to apply. + */ + void applyFlagUpdates(List flagUpdates); + + /** + * First removes all flags from the store, then applies a list of flag updates to the + * FlagStore. + * + * @param flagUpdates The list of FlagUpdates to apply. + */ + void clearAndApplyFlagUpdates(List flagUpdates); + + /** + * Gets all flags currently in the store. + * + * @return A collection of the stored Flags. + */ + Collection getAllFlags(); + + /** + * Register a listener to be called on any updates to the store. If a listener is already + * registered, it will be replaced with the argument listener. The FlagStore implementation is + * not guaranteed to retain a strong reference to the listener. + * + * @param storeUpdatedListener The listener to be called on store updates. + */ + void registerOnStoreUpdatedListener(StoreUpdatedListener storeUpdatedListener); + + /** + * Remove the currently registered listener if one exists. + */ + void unregisterOnStoreUpdatedListener(); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreFactory.java new file mode 100644 index 00000000..f9335499 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreFactory.java @@ -0,0 +1,19 @@ +package com.launchdarkly.android; + +import android.support.annotation.NonNull; + +/** + * This interface is used to provide a mechanism for a FlagStoreManager to create FlagStores without + * being dependent on a concrete FlagStore class. + */ +interface FlagStoreFactory { + + /** + * Create a new flag store + * + * @param identifier identifier to associate all flags under + * @return A new instance of a FlagStore backed by a concrete implementation. + */ + FlagStore createFlagStore(@NonNull String identifier); + +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreManager.java new file mode 100644 index 00000000..124a6cfa --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreManager.java @@ -0,0 +1,64 @@ +package com.launchdarkly.android; + +import java.util.Collection; + +/** + * A FlagStoreManager is responsible for managing FlagStores for active and recently active users, + * as well as providing flagKey specific update callbacks. + */ +interface FlagStoreManager { + + /** + * Loads the FlagStore for the particular userKey. If too many users have a locally cached + * FlagStore, deletes the oldest. + * + * @param userKey The key representing the user to switch to + */ + void switchToUser(String userKey); + + /** + * Gets the current user's flag store. + * + * @return The flag store for the current user. + */ + FlagStore getCurrentUserStore(); + + /** + * Register a listener to be called when a flag with the given key is created or updated. + * Multiple listeners can be registered to a single key. + * + * @param key Flag key to register the listener to. + * @param listener The listener to be called when the flag is updated. + */ + void registerListener(String key, FeatureFlagChangeListener listener); + + /** + * Unregister a specific listener registered to the given key. + * + * @param key Flag key to unregister the listener from. + * @param listener The specific listener to be unregistered. + */ + void unRegisterListener(String key, FeatureFlagChangeListener listener); + + /** + * Register a listener to be called whenever new flag data is received. + * + * @param listener The listener to be called new flag data is received. + */ + void registerAllFlagsListener(LDAllFlagsListener listener); + + /** + * Unregister a listener previously registered with registerAllFlagsListener. + * + * @param listener The specific listener to be unregistered. + */ + void unregisterAllFlagsListener(LDAllFlagsListener listener); + + /** + * Gets all the listeners currently registered to the given key. + * + * @param key The key to return the listeners for. + * @return A collection of listeners registered to the key. + */ + Collection getListenersByKey(String key); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreUpdateType.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreUpdateType.java new file mode 100644 index 00000000..8ba96484 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagStoreUpdateType.java @@ -0,0 +1,19 @@ +package com.launchdarkly.android; + +/** + * Types of updates that a FlagStore can report + */ +enum FlagStoreUpdateType { + /** + * The flag was deleted + */ + FLAG_DELETED, + /** + * The flag has been updated or replaced + */ + FLAG_UPDATED, + /** + * A new flag has been created + */ + FLAG_CREATED +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagUpdate.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagUpdate.java new file mode 100644 index 00000000..32b9620c --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagUpdate.java @@ -0,0 +1,26 @@ +package com.launchdarkly.android; + +/** + * Interfaces for classes that are tied to a flagKey and can take an existing flag and determine + * whether it should be updated/deleted/left the same based on its update payload. + */ +interface FlagUpdate { + + /** + * Given an existing Flag retrieved by the flagKey returned by flagToUpdate(), updateFlag should + * return null if the flag is to be deleted, a new Flag if the flag should be replaced by the + * new Flag, or the before Flag if the flag should be left the same. + * + * @param before An existing Flag associated with flagKey from flagToUpdate() + * @return null, a new Flag, or the before Flag. + */ + Flag updateFlag(Flag before); + + /** + * Get the key of the flag that this FlagUpdate is intended to update. + * + * @return The key of the flag to be updated. + */ + String flagToUpdate(); + +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponse.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponse.java new file mode 100644 index 00000000..0ed35ead --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponse.java @@ -0,0 +1,22 @@ +package com.launchdarkly.android; + +import android.support.annotation.NonNull; + +import com.google.gson.annotations.JsonAdapter; + +import java.util.List; + +@JsonAdapter(FlagsResponseSerialization.class) +class FlagsResponse { + @NonNull + private final List flags; + + public FlagsResponse(@NonNull List flags) { + this.flags = flags; + } + + @NonNull + public List getFlags() { + return flags; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponseSerialization.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponseSerialization.java new file mode 100644 index 00000000..8277ecff --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/FlagsResponseSerialization.java @@ -0,0 +1,36 @@ +package com.launchdarkly.android; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Map; + +class FlagsResponseSerialization implements JsonDeserializer { + @Override + public FlagsResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject o = json.getAsJsonObject(); + if (o == null) { + return null; + } + ArrayList flags = new ArrayList<>(); + for (Map.Entry flagJson : o.entrySet()) { + String flagKey = flagJson.getKey(); + JsonElement flagBody = flagJson.getValue(); + JsonObject flagBodyObject = flagBody.getAsJsonObject(); + if (flagBodyObject != null) { + flagBodyObject.addProperty("key", flagKey); + } + Flag flag = context.deserialize(flagBodyObject, Flag.class); + if (flag != null) { + flags.add(flag); + } + } + + return new FlagsResponse(flags); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/GsonCache.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/GsonCache.java new file mode 100644 index 00000000..5e7a395d --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/GsonCache.java @@ -0,0 +1,18 @@ +package com.launchdarkly.android; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +class GsonCache { + + private static final Gson gson = createGson(); + + static Gson getGson() { + return gson; + } + + private static Gson createGson() { + GsonBuilder gsonBuilder = new GsonBuilder(); + return gsonBuilder.create(); + } +} 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 898c355f..b714040e 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 @@ -6,8 +6,6 @@ import com.google.gson.JsonObject; import com.google.gson.JsonParser; -import com.launchdarkly.android.tls.ModernTLSSocketFactory; -import com.launchdarkly.android.tls.TLSUtils; import java.io.File; import java.io.IOException; 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 c89c5ff4..7969ec2f 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 @@ -10,8 +10,6 @@ import android.support.annotation.VisibleForTesting; import com.google.gson.JsonElement; -import com.launchdarkly.android.flagstore.Flag; -import com.launchdarkly.android.gson.GsonCache; import java.io.Closeable; import java.io.IOException; @@ -31,7 +29,7 @@ import timber.log.Timber; -import static com.launchdarkly.android.tls.TLSUtils.patchTLSIfNeeded; +import static com.launchdarkly.android.TLSUtils.patchTLSIfNeeded; /** * Client for accessing LaunchDarkly's Feature Flag system. This class enforces a singleton pattern. 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 4fe73fec..f1cd00de 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 @@ -119,8 +119,8 @@ public interface LDClientInterface extends Closeable { /** * 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 in - * {@link LDConfig.Builder#evaluationReasons}. Otherwise, the {@code reason} property of the result + * 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 @@ -148,8 +148,8 @@ public interface LDClientInterface extends Closeable { /** * 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 in - * {@link LDConfig.Builder#evaluationReasons}. Otherwise, the {@code reason} property of the result + * 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 @@ -177,8 +177,8 @@ public interface LDClientInterface extends Closeable { /** * 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 in - * {@link LDConfig.Builder#evaluationReasons}. Otherwise, the {@code reason} property of the result + * 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 @@ -206,8 +206,8 @@ public interface LDClientInterface extends Closeable { /** * 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 in - * {@link LDConfig.Builder#evaluationReasons}. Otherwise, the {@code reason} property of the result + * 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 @@ -234,8 +234,8 @@ public interface LDClientInterface extends Closeable { /** * 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 in - * {@link LDConfig.Builder#evaluationReasons}. Otherwise, the {@code reason} property of the result + * 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 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 64af036a..bb5c87f8 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 @@ -198,6 +198,16 @@ public boolean isEvaluationReasons() { return evaluationReasons; } + /** + * A builder that helps construct + * {@link LDConfig} objects. Builder calls can be chained, enabling the following pattern: + *
+     * LDConfig config = new LDConfig.Builder()
+     *          .setMobileKey("mobile-key")
+     *          .setEvaluationReasons(true)
+     *          .build();
+     * 
+ */ public static class Builder { private String mobileKey; private Map secondaryMobileKeys; 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 caefea1b..a0c2b04a 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 @@ -96,6 +96,8 @@ * * @author Takahiko Kawasaki */ +@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"}) +@Deprecated public enum LDCountryCode { /** * Ascension Island diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailedFuture.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailedFuture.java deleted file mode 100644 index 3ea10fba..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailedFuture.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.launchdarkly.android; - -import android.support.annotation.NonNull; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -class LDFailedFuture implements Future { - private final Throwable error; - - LDFailedFuture(Throwable error) { - this.error = error; - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return true; - } - - @Override - public T get() throws ExecutionException { - throw new ExecutionException(error); - } - - @Override - public T get(long timeout, @NonNull TimeUnit unit) throws ExecutionException { - throw new ExecutionException(error); - } -} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailure.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailure.java index ada10e5a..20558731 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailure.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailure.java @@ -3,7 +3,9 @@ import android.support.annotation.NonNull; import com.google.gson.annotations.Expose; +import com.google.gson.annotations.JsonAdapter; +@JsonAdapter(LDFailureSerialization.class) public class LDFailure extends LaunchDarklyException { public enum FailureType { INVALID_RESPONSE_BODY, diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailureSerialization.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailureSerialization.java new file mode 100644 index 00000000..338d6a64 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFailureSerialization.java @@ -0,0 +1,49 @@ +package com.launchdarkly.android; + +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.launchdarkly.android.LDFailure; +import com.launchdarkly.android.LDInvalidResponseCodeFailure; + +import java.lang.reflect.Type; + +class LDFailureSerialization implements JsonSerializer, JsonDeserializer { + @Override + public LDFailure deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + JsonObject in = json.getAsJsonObject(); + LDFailure.FailureType failureType = context.deserialize(in.get("failureType"), LDFailure.FailureType.class); + String message = in.getAsJsonPrimitive("message").getAsString(); + if (failureType == LDFailure.FailureType.UNEXPECTED_RESPONSE_CODE) { + int responseCode = in.getAsJsonPrimitive("responseCode").getAsInt(); + boolean retryable = in.getAsJsonPrimitive("retryable").getAsBoolean(); + return new LDInvalidResponseCodeFailure(message, responseCode, retryable); + } else { + return new LDFailure(message, failureType); + } + } + + @Override + public JsonElement serialize(LDFailure src, Type typeOfSrc, JsonSerializationContext context) { + if (src == null) { + return null; + } + try { + JsonObject jsonObject = new JsonObject(); + jsonObject.add("failureType", context.serialize(src.getFailureType())); + jsonObject.addProperty("message", src.getMessage()); + if (src instanceof LDInvalidResponseCodeFailure) { + LDInvalidResponseCodeFailure fail = (LDInvalidResponseCodeFailure) src; + jsonObject.addProperty("responseCode", fail.getResponseCode()); + jsonObject.addProperty("retryable", fail.isRetryable()); + } + return jsonObject; + } catch (Exception unused) { + return null; + } + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFutures.java similarity index 66% rename from launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java rename to launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFutures.java index d46ba478..8d76693e 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDAwaitFuture.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDFutures.java @@ -9,6 +9,72 @@ import timber.log.Timber; +class LDSuccessFuture implements Future { + private final T result; + + LDSuccessFuture(T result) { + this.result = result; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() { + return result; + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) { + return result; + } +} + +class LDFailedFuture implements Future { + private final Throwable error; + + LDFailedFuture(Throwable error) { + this.error = error; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public T get() throws ExecutionException { + throw new ExecutionException(error); + } + + @Override + public T get(long timeout, @NonNull TimeUnit unit) throws ExecutionException { + throw new ExecutionException(error); + } +} + class LDAwaitFuture implements Future { private volatile T result = null; private volatile Throwable error = null; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDSuccessFuture.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDSuccessFuture.java deleted file mode 100644 index f9267c59..00000000 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/LDSuccessFuture.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.launchdarkly.android; - -import android.support.annotation.NonNull; - -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; - -class LDSuccessFuture implements Future { - private final T result; - - LDSuccessFuture(T result) { - this.result = result; - } - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return false; - } - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public boolean isDone() { - return true; - } - - @Override - public T get() { - return result; - } - - @Override - public T get(long timeout, @NonNull TimeUnit unit) { - return result; - } -} 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 80549e1c..d1d57fa1 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 @@ -211,6 +211,7 @@ public static class Builder { private String email; private String name; private String avatar; + @SuppressWarnings("deprecation") private LDCountryCode country; private final Map custom; @@ -232,6 +233,7 @@ public Builder(String key) { privateAttributeNames = new HashSet<>(); } + @SuppressWarnings("deprecation") public Builder(LDUser user) { this.key = user.getKey(); this.anonymous = user.getAnonymous(); @@ -282,10 +284,12 @@ public Builder privateSecondary(String s) { } /** - * Set the country for a user. The country should be a valid ISO 3166-1 alpha-2 or alpha-3 code. If * it is not a valid ISO-3166-1 code, an attempt will be made to look up the country by its - * name. If that fails, a warning will be logged, and the country will not be set. + * name. If that fails, a warning will be logged, and the country will not be set. In the + * next major release (3.0.0) the SDK will not attempt attempt this lookup, and instead + * treat the country field as a normal String. * * @param s the country for the user * @return the builder @@ -296,11 +300,12 @@ public Builder country(String s) { } /** - * Set the country for a user. The country should be a valid ISO 3166-1 alpha-2 or alpha-3 code. If * it is not a valid ISO-3166-1 code, an attempt will be made to look up the country by its - * name. If that fails, a warning will be logged, and the country will not be set. Private - * attributes are not recorded in events. + * name. If that fails, a warning will be logged, and the country will not be set. In the + * next major release (3.0.0) the SDK will not attempt attempt this lookup, and instead + * treat the country field as a normal String. Private attributes are not recorded in events. * * @param s the country for the user * @return the builder @@ -310,6 +315,8 @@ public Builder privateCountry(String s) { return country(s); } + + @SuppressWarnings("deprecation") private LDCountryCode countryCode(String s) { LDCountryCode countryCode = LDCountryCode.getByCode(s, false); @@ -341,7 +348,11 @@ private LDCountryCode countryCode(String s) { * * @param country the country for the user * @return the builder - */ + * @deprecated As of version 2.10.0, in 3.0.0 the SDK will no longer include the + * LDCountryCode class. Applications should use {@link #country(String)} instead. + * */ + @Deprecated + @SuppressWarnings("deprecation") public Builder country(LDCountryCode country) { this.country = country; return this; @@ -352,7 +363,11 @@ public Builder country(LDCountryCode country) { * * @param country the country for the user * @return the builder + * @deprecated As of version 2.10.0, in 3.0.0 the SDK will no longer include the + * LDCountryCode class. Applications should use {@link #privateCountry(String)} instead. */ + @Deprecated + @SuppressWarnings("deprecation") public Builder privateCountry(LDCountryCode country) { privateAttributeNames.add(COUNTRY); return country(country); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Migration.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Migration.java index 609ec8fb..db27b187 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Migration.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Migration.java @@ -7,7 +7,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonSyntaxException; -import com.launchdarkly.android.gson.GsonCache; import java.io.File; import java.util.ArrayList; 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 9a0e1405..449caa43 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 @@ -9,6 +9,9 @@ import timber.log.Timber; +/** + * Used internally by the SDK. + */ public class PollingUpdater extends BroadcastReceiver { private static int backgroundPollingIntervalMillis = LDConfig.DEFAULT_BACKGROUND_POLLING_INTERVAL_MILLIS; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStore.java new file mode 100644 index 00000000..91dac1ae --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStore.java @@ -0,0 +1,159 @@ +package com.launchdarkly.android; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Pair; + +import java.io.File; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import timber.log.Timber; + +class SharedPrefsFlagStore implements FlagStore { + + private static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; + private final String prefsKey; + private final Application application; + private SharedPreferences sharedPreferences; + private WeakReference listenerWeakReference; + + SharedPrefsFlagStore(@NonNull Application application, @NonNull String identifier) { + this.application = application; + this.prefsKey = SHARED_PREFS_BASE_KEY + identifier + "-flags"; + this.sharedPreferences = application.getSharedPreferences(prefsKey, Context.MODE_PRIVATE); + this.listenerWeakReference = new WeakReference<>(null); + } + + @SuppressLint("ApplySharedPref") + @Override + public void delete() { + sharedPreferences.edit().clear().commit(); + sharedPreferences = null; + + File file = new File(application.getFilesDir().getParent() + "/shared_prefs/" + prefsKey + ".xml"); + Timber.i("Deleting SharedPrefs file:%s", file.getAbsolutePath()); + + //noinspection ResultOfMethodCallIgnored + file.delete(); + } + + @Override + public void clear() { + sharedPreferences.edit().clear().apply(); + } + + @Override + public boolean containsKey(String key) { + return sharedPreferences.contains(key); + } + + @Nullable + @Override + public Flag getFlag(String flagKey) { + return Util.sharedPrefsGetGson(sharedPreferences, Flag.class, flagKey); + } + + private Pair applyFlagUpdateNoCommit(@NonNull SharedPreferences.Editor editor, @NonNull FlagUpdate flagUpdate) { + String flagKey = flagUpdate.flagToUpdate(); + if (flagKey == null) { + return null; + } + Flag flag = getFlag(flagKey); + Flag newFlag = flagUpdate.updateFlag(flag); + if (flag != null && newFlag == null) { + editor.remove(flagKey); + return new Pair<>(flagKey, FlagStoreUpdateType.FLAG_DELETED); + } else if (flag == null && newFlag != null) { + String flagData = GsonCache.getGson().toJson(newFlag); + editor.putString(flagKey, flagData); + return new Pair<>(flagKey, FlagStoreUpdateType.FLAG_CREATED); + } else if (flag != newFlag) { + String flagData = GsonCache.getGson().toJson(newFlag); + editor.putString(flagKey, flagData); + return new Pair<>(flagKey, FlagStoreUpdateType.FLAG_UPDATED); + } + return null; + } + + @Override + public void applyFlagUpdate(FlagUpdate flagUpdate) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + Pair update = applyFlagUpdateNoCommit(editor, flagUpdate); + editor.apply(); + StoreUpdatedListener storeUpdatedListener = listenerWeakReference.get(); + if (update != null && storeUpdatedListener != null) { + storeUpdatedListener.onStoreUpdate(Collections.singletonList(new Pair<>(update.first, update.second))); + } + } + + private void informListenerOfUpdateList(List> updates) { + StoreUpdatedListener storeUpdatedListener = listenerWeakReference.get(); + if (storeUpdatedListener != null) { + storeUpdatedListener.onStoreUpdate(updates); + } + } + + @Override + public void applyFlagUpdates(List flagUpdates) { + SharedPreferences.Editor editor = sharedPreferences.edit(); + ArrayList> updates = new ArrayList<>(); + for (FlagUpdate flagUpdate : flagUpdates) { + Pair update = applyFlagUpdateNoCommit(editor, flagUpdate); + if (update != null) { + updates.add(update); + } + } + editor.apply(); + informListenerOfUpdateList(updates); + } + + @Override + public void clearAndApplyFlagUpdates(List flagUpdates) { + Set clearedKeys = sharedPreferences.getAll().keySet(); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + ArrayList> updates = new ArrayList<>(); + for (FlagUpdate flagUpdate : flagUpdates) { + String flagKey = flagUpdate.flagToUpdate(); + if (flagKey == null) { + continue; + } + Flag newFlag = flagUpdate.updateFlag(null); + if (newFlag != null) { + String flagData = GsonCache.getGson().toJson(newFlag); + editor.putString(flagKey, flagData); + clearedKeys.remove(flagKey); + updates.add(new Pair<>(flagKey, FlagStoreUpdateType.FLAG_CREATED)); + } + } + editor.apply(); + for (String clearedKey : clearedKeys) { + updates.add(new Pair<>(clearedKey, FlagStoreUpdateType.FLAG_DELETED)); + } + informListenerOfUpdateList(updates); + } + + @Override + public Collection getAllFlags() { + return Util.sharedPrefsGetAllGson(sharedPreferences, Flag.class).values(); + } + + @Override + public void registerOnStoreUpdatedListener(StoreUpdatedListener storeUpdatedListener) { + listenerWeakReference = new WeakReference<>(storeUpdatedListener); + } + + @Override + public void unregisterOnStoreUpdatedListener() { + listenerWeakReference.clear(); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreFactory.java new file mode 100644 index 00000000..d4e63332 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreFactory.java @@ -0,0 +1,18 @@ +package com.launchdarkly.android; + +import android.app.Application; +import android.support.annotation.NonNull; + +class SharedPrefsFlagStoreFactory implements FlagStoreFactory { + + private final Application application; + + SharedPrefsFlagStoreFactory(@NonNull Application application) { + this.application = application; + } + + @Override + public FlagStore createFlagStore(@NonNull String identifier) { + return new SharedPrefsFlagStore(application, identifier); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreManager.java new file mode 100644 index 00000000..e132a09e --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/SharedPrefsFlagStoreManager.java @@ -0,0 +1,189 @@ +package com.launchdarkly.android; + +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; + +import timber.log.Timber; + +class SharedPrefsFlagStoreManager implements FlagStoreManager, StoreUpdatedListener { + + private static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; + private static final int MAX_USERS = 5; + + @NonNull + private final FlagStoreFactory flagStoreFactory; + @NonNull + private final String mobileKey; + + private FlagStore currentFlagStore; + private final SharedPreferences usersSharedPrefs; + private final ConcurrentHashMap> listeners; + private final CopyOnWriteArrayList allFlagsListeners; + + SharedPrefsFlagStoreManager(@NonNull Application application, + @NonNull String mobileKey, + @NonNull FlagStoreFactory flagStoreFactory) { + this.mobileKey = mobileKey; + this.flagStoreFactory = flagStoreFactory; + this.usersSharedPrefs = application.getSharedPreferences(SHARED_PREFS_BASE_KEY + mobileKey + "-users", Context.MODE_PRIVATE); + this.listeners = new ConcurrentHashMap<>(); + this.allFlagsListeners = new CopyOnWriteArrayList<>(); + } + + @Override + public void switchToUser(String userKey) { + if (currentFlagStore != null) { + currentFlagStore.unregisterOnStoreUpdatedListener(); + } + currentFlagStore = flagStoreFactory.createFlagStore(storeIdentifierForUser(userKey)); + currentFlagStore.registerOnStoreUpdatedListener(this); + + // Store the user's key and the current time in usersSharedPrefs so it can be removed when + // MAX_USERS is exceeded. + usersSharedPrefs.edit() + .putLong(userKey, System.currentTimeMillis()) + .apply(); + + int usersStored = usersSharedPrefs.getAll().size(); + if (usersStored > MAX_USERS) { + Iterator oldestFirstUsers = getAllUsers().iterator(); + // Remove oldest users until we are at MAX_USERS. + while (usersStored-- > MAX_USERS) { + String removed = oldestFirstUsers.next(); + Timber.d("Exceeded max # of users: [%s] Removing user: [%s]", MAX_USERS, removed); + // Load FlagStore for oldest user and delete it. + flagStoreFactory.createFlagStore(storeIdentifierForUser(removed)).delete(); + // Remove entry from usersSharedPrefs. + usersSharedPrefs.edit().remove(removed).apply(); + } + } + } + + private String storeIdentifierForUser(String userKey) { + return mobileKey + userKey; + } + + @Override + public FlagStore getCurrentUserStore() { + return currentFlagStore; + } + + @Override + public void registerListener(String key, FeatureFlagChangeListener listener) { + Map backingMap = new ConcurrentHashMap<>(); + Set newSet = Collections.newSetFromMap(backingMap); + newSet.add(listener); + Set oldSet = listeners.putIfAbsent(key, newSet); + if (oldSet != null) { + oldSet.add(listener); + Timber.d("Added listener. Total count: [%s]", oldSet.size()); + } else { + Timber.d("Added listener. Total count: 1"); + } + } + + @Override + public void unRegisterListener(String key, FeatureFlagChangeListener listener) { + Set keySet = listeners.get(key); + if (keySet != null) { + boolean removed = keySet.remove(listener); + if (removed) { + Timber.d("Removing listener for key: [%s]", key); + } + } + } + + @Override + public void registerAllFlagsListener(LDAllFlagsListener listener) { + allFlagsListeners.add(listener); + } + + @Override + public void unregisterAllFlagsListener(LDAllFlagsListener listener) { + allFlagsListeners.remove(listener); + } + + // Gets all users sorted by creation time (oldest first) + private Collection getAllUsers() { + Map all = usersSharedPrefs.getAll(); + TreeMap sortedMap = new TreeMap<>(); + //get typed versions of the users' timestamps and insert into sorted TreeMap + for (String k : all.keySet()) { + try { + sortedMap.put((Long) all.get(k), k); + Timber.d("Found user: %s", userAndTimeStampToHumanReadableString(k, (Long) all.get(k))); + } catch (ClassCastException cce) { + Timber.e(cce, "Unexpected type! This is not good"); + } + } + return sortedMap.values(); + } + + private static String userAndTimeStampToHumanReadableString(String userSharedPrefsKey, Long timestamp) { + return userSharedPrefsKey + " [" + userSharedPrefsKey + "] timestamp: [" + timestamp + "]" + " [" + new Date(timestamp) + "]"; + } + + private void dispatchStoreUpdateCallback(final String flagKey, final FlagStoreUpdateType flagStoreUpdateType) { + // We make sure to call listener callbacks on the main thread, as we consistently did so in + // the past by virtue of using SharedPreferences to implement the callbacks. + if (Looper.myLooper() == Looper.getMainLooper()) { + // Get the listeners for the specific key + Set keySet = listeners.get(flagKey); + // If there are any listeners for this key + if (keySet != null) { + // We only call the listener if the flag is a new flag or updated. + if (flagStoreUpdateType != FlagStoreUpdateType.FLAG_DELETED) { + for (FeatureFlagChangeListener listener : keySet) { + listener.onFeatureFlagChange(flagKey); + } + } else { + // When flag is deleted we remove the corresponding listeners + listeners.remove(flagKey); + } + } + } else { + // Call ourselves on the main thread + new Handler(Looper.getMainLooper()).post(new Runnable() { + @Override + public void run() { + dispatchStoreUpdateCallback(flagKey, flagStoreUpdateType); + } + }); + } + } + + @Override + public void onStoreUpdate(List> updates) { + List flagKeys = new ArrayList<>(); + for (Pair update : updates) { + flagKeys.add(update.first); + dispatchStoreUpdateCallback(update.first, update.second); + } + for (LDAllFlagsListener allFlagsListener : allFlagsListeners) { + allFlagsListener.onChange(flagKeys); + } + } + + public Collection getListenersByKey(String key) { + Set res = listeners.get(key); + return res == null ? new HashSet() : res; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StoreUpdatedListener.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StoreUpdatedListener.java new file mode 100644 index 00000000..e12d31f6 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/StoreUpdatedListener.java @@ -0,0 +1,17 @@ +package com.launchdarkly.android; + +import android.util.Pair; + +import java.util.List; + +/** + * Listener interface for receiving FlagStore update callbacks + */ +interface StoreUpdatedListener { + /** + * Called by a FlagStore when the store is updated. + * + * @param updates Pairs of flag keys that were updated and the type of update that occurred. + */ + void onStoreUpdate(List> updates); +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/TLSUtils.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/TLSUtils.java new file mode 100644 index 00000000..e9ccefe3 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/TLSUtils.java @@ -0,0 +1,167 @@ +package com.launchdarkly.android; + +import android.app.Application; +import android.support.annotation.NonNull; + +import com.google.android.gms.common.GooglePlayServicesNotAvailableException; +import com.google.android.gms.common.GooglePlayServicesRepairableException; +import com.google.android.gms.security.ProviderInstaller; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.GeneralSecurityException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +import okhttp3.CipherSuite; +import okhttp3.Handshake; +import okhttp3.Response; +import okhttp3.TlsVersion; +import timber.log.Timber; + +class TLSUtils { + + static X509TrustManager defaultTrustManager() throws GeneralSecurityException { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance( + TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) { + throw new IllegalStateException("Unexpected default trust managers:" + + Arrays.toString(trustManagers)); + } + return (X509TrustManager) trustManagers[0]; + } + + static void patchTLSIfNeeded(Application application) { + try { + SSLContext.getInstance("TLSv1.2"); + } catch (NoSuchAlgorithmException e) { + Timber.w("No TLSv1.2 implementation available, attempting patch."); + try { + ProviderInstaller.installIfNeeded(application.getApplicationContext()); + } catch (GooglePlayServicesRepairableException e1) { + Timber.w("Patch failed, Google Play Services too old."); + } catch (GooglePlayServicesNotAvailableException e1) { + Timber.w("Patch failed, no Google Play Services available."); + } + } + } +} + +/** + * Intercepts the SSL connection and prints TLS version and CipherSuite in the log. + */ +class SSLHandshakeInterceptor implements okhttp3.Interceptor { + + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + final Response response = chain.proceed(chain.request()); + printTlsAndCipherSuiteInfo(response); + return response; + } + + private void printTlsAndCipherSuiteInfo(Response response) { + if (response != null) { + Handshake handshake = response.handshake(); + if (handshake != null) { + final CipherSuite cipherSuite = handshake.cipherSuite(); + final TlsVersion tlsVersion = handshake.tlsVersion(); + Timber.v("TLS: %s, CipherSuite: %s", tlsVersion, cipherSuite); + } + } + } +} + +/** + * An {@link SSLSocketFactory} that tries to ensure modern TLS versions are used. + */ +class ModernTLSSocketFactory extends SSLSocketFactory { + private static final String TLS_1_2 = "TLSv1.2"; + private static final String TLS_1_1 = "TLSv1.1"; + private static final String TLS_1 = "TLSv1"; + + private SSLSocketFactory defaultSocketFactory; + + public ModernTLSSocketFactory() throws NoSuchAlgorithmException, KeyManagementException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + this.defaultSocketFactory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return this.defaultSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return this.defaultSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException { + return setModernTlsVersionsOnSocket(this.defaultSocketFactory.createSocket(socket, s, i, b)); + } + + @Override + public Socket createSocket(String s, int i) throws IOException, UnknownHostException { + return setModernTlsVersionsOnSocket(this.defaultSocketFactory.createSocket(s, i)); + } + + @Override + public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException { + return setModernTlsVersionsOnSocket(this.defaultSocketFactory.createSocket(s, i, inetAddress, i1)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i) throws IOException { + return setModernTlsVersionsOnSocket(this.defaultSocketFactory.createSocket(inetAddress, i)); + } + + @Override + public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException { + return setModernTlsVersionsOnSocket(this.defaultSocketFactory.createSocket(inetAddress, i, inetAddress1, i1)); + } + + /** + * If either of TLSv1.2, TLSv1.1, or TLSv1 are supported, make them the only enabled protocols (listing in that order). + *

+ * If the socket does not make these modern TLS protocols available at all, then just return the socket unchanged. + * + * @param s the socket + * @return the socket with modern TLS protocols enabled if possible + */ + private static Socket setModernTlsVersionsOnSocket(Socket s) { + if (s instanceof SSLSocket) { + List defaultEnabledProtocols = Arrays.asList(((SSLSocket) s).getSupportedProtocols()); + ArrayList newEnabledProtocols = new ArrayList<>(); + if (defaultEnabledProtocols.contains(TLS_1_2)) { + newEnabledProtocols.add(TLS_1_2); + } + if (defaultEnabledProtocols.contains(TLS_1_1)) { + newEnabledProtocols.add(TLS_1_1); + } + if (defaultEnabledProtocols.contains(TLS_1)) { + newEnabledProtocols.add(TLS_1); + } + if (newEnabledProtocols.size() > 0) { + ((SSLSocket) s).setEnabledProtocols(newEnabledProtocols.toArray(new String[0])); + } + } + return s; + } +} \ No newline at end of file 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 ede096d4..ce9f052d 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 @@ -1,12 +1,7 @@ package com.launchdarkly.android; -/* - Created by jamesthacker on 4/2/18. - */ - import android.os.Handler; import android.os.HandlerThread; -import android.os.Looper; import android.support.annotation.NonNull; import java.util.Random; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/UserSummaryEventSharedPreferences.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/UserSummaryEventSharedPreferences.java index 5ecdc4d3..687a12fb 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/UserSummaryEventSharedPreferences.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/UserSummaryEventSharedPreferences.java @@ -16,9 +16,8 @@ import timber.log.Timber; /** - * Created by jamesthacker on 4/12/18. + * Used internally by the SDK. */ - public class UserSummaryEventSharedPreferences implements SummaryEventSharedPreferences { private final SharedPreferences sharedPreferences; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Util.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Util.java index 54f35466..8ac3102f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Util.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/Util.java @@ -7,13 +7,14 @@ import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import com.launchdarkly.android.gson.GsonCache; - import java.util.HashMap; import java.util.Map; import timber.log.Timber; +/** + * Used internally by the SDK. + */ public class Util { /** @@ -80,4 +81,23 @@ public interface ResultCallback { void onSuccess(T result); void onError(Throwable e); } + + /** + * Tests whether an HTTP error status represents a condition that might resolve on its own if we retry. + * @param statusCode the HTTP status + * @return true if retrying makes sense; false if it should be considered a permanent failure + */ + static boolean isHttpErrorRecoverable(int statusCode) { + if (statusCode >= 400 && statusCode < 500) { + switch (statusCode) { + case 400: // bad request + case 408: // request timeout + case 429: // too many requests + return true; + default: + return false; // all other 4xx errors are unrecoverable + } + } + return true; + } } 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 7f8e7b1c..31549acd 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 @@ -5,7 +5,6 @@ import com.google.gson.JsonElement; import com.google.gson.JsonPrimitive; -import com.launchdarkly.android.gson.GsonCache; import timber.log.Timber; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/Flag.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/Flag.java index 50745ab9..4cc17d92 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/Flag.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/Flag.java @@ -5,6 +5,8 @@ import com.google.gson.JsonElement; import com.launchdarkly.android.EvaluationReason; +@SuppressWarnings("deprecation") +@Deprecated public class Flag implements FlagUpdate, FlagInterface { @NonNull diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagInterface.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagInterface.java index 4362d5e2..78b9bf44 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagInterface.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagInterface.java @@ -8,6 +8,7 @@ /** * Public interface for a Flag, to be used if exposing Flag model to public API methods. */ +@Deprecated public interface FlagInterface { /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStore.java index 3e2667a6..9078be84 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStore.java @@ -1,15 +1,17 @@ package com.launchdarkly.android.flagstore; +import android.support.annotation.Nullable; + import java.util.Collection; import java.util.List; -import javax.annotation.Nullable; - /** * A FlagStore supports getting individual or collections of flag updates and updating an underlying * persistent store. Individual flags can be retrieved by a flagKey, or all flags retrieved. Allows * replacing backing store for flags at a future date, as well as mocking for unit testing. */ +@SuppressWarnings("deprecation") +@Deprecated public interface FlagStore { /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreFactory.java index 48fb3db0..0c2b5655 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreFactory.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreFactory.java @@ -6,6 +6,8 @@ * This interface is used to provide a mechanism for a FlagStoreManager to create FlagStores without * being dependent on a concrete FlagStore class. */ +@SuppressWarnings("deprecation") +@Deprecated public interface FlagStoreFactory { /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreManager.java index d9c67a52..4193b457 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreManager.java @@ -9,6 +9,8 @@ * A FlagStoreManager is responsible for managing FlagStores for active and recently active users, * as well as providing flagKey specific update callbacks. */ +@SuppressWarnings("deprecation") +@Deprecated public interface FlagStoreManager { /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreUpdateType.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreUpdateType.java index 5885d06f..6c206e73 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreUpdateType.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagStoreUpdateType.java @@ -3,6 +3,7 @@ /** * Types of updates that a FlagStore can report */ +@Deprecated public enum FlagStoreUpdateType { /** * The flag was deleted diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagUpdate.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagUpdate.java index 9855284c..5f174605 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagUpdate.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/FlagUpdate.java @@ -4,6 +4,8 @@ * Interfaces for classes that are tied to a flagKey and can take an existing flag and determine * whether it should be updated/deleted/left the same based on its update payload. */ +@SuppressWarnings("deprecation") +@Deprecated public interface FlagUpdate { /** diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/StoreUpdatedListener.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/StoreUpdatedListener.java index 91618de7..9c7b46b5 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/StoreUpdatedListener.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/StoreUpdatedListener.java @@ -7,6 +7,8 @@ /** * Listener interface for receiving FlagStore update callbacks */ +@SuppressWarnings("deprecation") +@Deprecated public interface StoreUpdatedListener { /** * Called by a FlagStore when the store is updated. diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStore.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStore.java index fa02afde..67d71da8 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStore.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStore.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Pair; import com.launchdarkly.android.Util; @@ -23,10 +24,10 @@ import java.util.List; import java.util.Set; -import javax.annotation.Nullable; - import timber.log.Timber; +@SuppressWarnings("deprecation") +@Deprecated class SharedPrefsFlagStore implements FlagStore { private static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactory.java index 1583c232..e4bea65d 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactory.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreFactory.java @@ -6,6 +6,8 @@ import com.launchdarkly.android.flagstore.FlagStore; import com.launchdarkly.android.flagstore.FlagStoreFactory; +@SuppressWarnings("deprecation") +@Deprecated public class SharedPrefsFlagStoreFactory implements FlagStoreFactory { private final Application application; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManager.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManager.java index 5fa8ef5d..054d6709 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManager.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/flagstore/sharedprefs/SharedPrefsFlagStoreManager.java @@ -31,6 +31,8 @@ import timber.log.Timber; +@SuppressWarnings("deprecation") +@Deprecated public class SharedPrefsFlagStoreManager implements FlagStoreManager, StoreUpdatedListener { private static final String SHARED_PREFS_BASE_KEY = "LaunchDarkly-"; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/FlagsResponseSerialization.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/FlagsResponseSerialization.java index 2ebd2578..d5bf0a7b 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/FlagsResponseSerialization.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/FlagsResponseSerialization.java @@ -12,6 +12,8 @@ import java.util.ArrayList; import java.util.Map; +@SuppressWarnings("deprecation") +@Deprecated class FlagsResponseSerialization implements JsonDeserializer { @Override public FlagsResponse deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/GsonCache.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/GsonCache.java index 9bcb3dff..3a0836ce 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/GsonCache.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/GsonCache.java @@ -5,6 +5,8 @@ import com.launchdarkly.android.LDFailure; import com.launchdarkly.android.response.FlagsResponse; +@SuppressWarnings("deprecation") +@Deprecated public class GsonCache { private static final Gson gson = createGson(); diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/LDFailureSerialization.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/LDFailureSerialization.java index d98df522..7c9ac1fd 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/LDFailureSerialization.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/gson/LDFailureSerialization.java @@ -12,6 +12,7 @@ import java.lang.reflect.Type; +@Deprecated public class LDFailureSerialization implements JsonSerializer, JsonDeserializer { @Override public LDFailure deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/DeleteFlagResponse.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/DeleteFlagResponse.java index 5abeb513..c268f513 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/DeleteFlagResponse.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/response/DeleteFlagResponse.java @@ -3,6 +3,8 @@ import com.launchdarkly.android.flagstore.Flag; import com.launchdarkly.android.flagstore.FlagUpdate; +@SuppressWarnings("deprecation") +@Deprecated public class DeleteFlagResponse implements FlagUpdate { private final String key; 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 7e677470..1036a085 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 @@ -9,6 +9,8 @@ /** * Used for cases where the server sends a collection of flags as a key-value object. */ +@SuppressWarnings("deprecation") +@Deprecated public class FlagsResponse { @NonNull private final List flags; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/ModernTLSSocketFactory.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/ModernTLSSocketFactory.java index 94b96d11..0a6f69b8 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/ModernTLSSocketFactory.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/ModernTLSSocketFactory.java @@ -17,6 +17,7 @@ /** * An {@link SSLSocketFactory} that tries to ensure modern TLS versions are used. */ +@Deprecated public class ModernTLSSocketFactory extends SSLSocketFactory { private static final String TLS_1_2 = "TLSv1.2"; private static final String TLS_1_1 = "TLSv1.1"; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/SSLHandshakeInterceptor.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/SSLHandshakeInterceptor.java index 406cd497..72eeec0f 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/SSLHandshakeInterceptor.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/SSLHandshakeInterceptor.java @@ -13,6 +13,7 @@ /** * Intercepts the SSL connection and prints TLS version and CipherSuite in the log. */ +@Deprecated public class SSLHandshakeInterceptor implements okhttp3.Interceptor { @Override diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/TLSUtils.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/TLSUtils.java index 006bcce5..99bc561a 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/TLSUtils.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/android/tls/TLSUtils.java @@ -18,6 +18,7 @@ import timber.log.Timber; +@Deprecated public class TLSUtils { public static X509TrustManager defaultTrustManager() throws GeneralSecurityException {