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

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

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

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

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


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

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


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

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


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

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

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


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

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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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