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 index 2506740b..1d00ea5e 100644 --- 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 @@ -1,6 +1,7 @@ package com.launchdarkly.sdk.android; import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; +import com.launchdarkly.sdk.android.integrations.DataSystemBuilder; import com.launchdarkly.sdk.android.integrations.EventProcessorBuilder; import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder; import com.launchdarkly.sdk.android.integrations.HttpConfigurationBuilder; @@ -221,4 +222,73 @@ public static HooksConfigurationBuilder hooks() { public static PluginsConfigurationBuilder plugins() { return new ComponentsImpl.PluginsConfigurationBuilderImpl(); } + + /** + * Returns a builder for configuring the data system. + *

+ * The data system controls how the SDK acquires and maintains feature flag data + * across different platform states (foreground, background, offline). It uses + * connection modes, each with its own pipeline of initializers and synchronizers. + *

+ * When called with no further customization, the data system uses sensible defaults: + * streaming with polling fallback in the foreground and low-frequency polling in the + * background. + *

+ * Example — opting in to use the default data system: + *


+     *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+     *         .mobileKey("my-key")
+     *         .dataSystem(Components.dataSystem())
+     *         .build();
+     * 
+ *

+ * Example — customize background polling to once every 6 hours: + *


+     *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+     *         .mobileKey("my-key")
+     *         .dataSystem(
+     *             Components.dataSystem()
+     *                 .customizeConnectionMode(ConnectionMode.BACKGROUND,
+     *                     DataSystemComponents.customMode()
+     *                         .initializers(DataSystemComponents.pollingInitializer())
+     *                         .synchronizers(
+     *                             DataSystemComponents.pollingSynchronizer()
+     *                                 .pollIntervalMillis(21_600_000))))
+     *         .build();
+     * 
+ *

+ * Example — use polling instead of streaming in the foreground: + *


+     *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+     *         .mobileKey("my-key")
+     *         .dataSystem(
+     *             Components.dataSystem()
+     *                 .foregroundConnectionMode(ConnectionMode.POLLING))
+     *         .build();
+     * 
+ *

+ * Example — disable automatic mode switching: + *


+     *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+     *         .mobileKey("my-key")
+     *         .dataSystem(
+     *             Components.dataSystem()
+     *                 .automaticModeSwitching(false)
+     *                 .foregroundConnectionMode(ConnectionMode.STREAMING))
+     *         .build();
+     * 
+ *

+ * Setting {@link LDConfig.Builder#dataSystem(DataSystemBuilder)} is mutually exclusive + * with {@link LDConfig.Builder#dataSource(ComponentConfigurer)}. The data system uses + * the FDv2 protocol, while {@code dataSource()} uses the legacy FDv1 protocol. + * + * @return a builder for configuring the data system + * @see DataSystemBuilder + * @see DataSystemComponents + * @see LDConfig.Builder#dataSystem(DataSystemBuilder) + */ + public static DataSystemBuilder dataSystem() { + return new DataSystemBuilder(); + } + } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionMode.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionMode.java index a256ec96..829a2056 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionMode.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectionMode.java @@ -2,8 +2,8 @@ /** * Enumerates the built-in FDv2 connection modes. Each mode maps to a - * {@link ModeDefinition} that specifies which initializers and synchronizers - * are active when the SDK is operating in that mode. + * pipeline of initializers and synchronizers that are active when the SDK + * is operating in that mode. *

* Not to be confused with {@link ConnectionInformation.ConnectionMode}, which * is the public FDv1 enum representing the SDK's current connection state @@ -13,18 +13,47 @@ * This is a closed enum — custom connection modes (spec 5.3.5 TBD) are not * supported in this release. *

- * Package-private — not part of the public SDK API. + * The SDK's {@link com.launchdarkly.sdk.android.integrations.DataSystemBuilder} + * allows you to customize which initializers and synchronizers run in each mode. + *

+ * On mobile, the SDK automatically transitions between modes based on + * platform state (foreground/background, network availability). The default + * resolution is: + *

* - * @see ModeDefinition - * @see ModeResolutionTable + * @see com.launchdarkly.sdk.android.integrations.DataSystemBuilder + * @see com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder */ -final class ConnectionMode { +public final class ConnectionMode { + + /** + * The SDK uses a streaming connection in the foreground, with polling as a fallback. + */ + public static final ConnectionMode STREAMING = new ConnectionMode("streaming"); + + /** + * The SDK polls for updates at a regular interval. + */ + public static final ConnectionMode POLLING = new ConnectionMode("polling"); + + /** + * The SDK does not make any network requests. It may still serve cached data. + */ + public static final ConnectionMode OFFLINE = new ConnectionMode("offline"); + + /** + * The SDK makes a single poll request and then stops. + */ + public static final ConnectionMode ONE_SHOT = new ConnectionMode("one-shot"); - static final ConnectionMode STREAMING = new ConnectionMode("streaming"); - static final ConnectionMode POLLING = new ConnectionMode("polling"); - static final ConnectionMode OFFLINE = new ConnectionMode("offline"); - static final ConnectionMode ONE_SHOT = new ConnectionMode("one-shot"); - static final ConnectionMode BACKGROUND = new ConnectionMode("background"); + /** + * The SDK polls at a low frequency while the application is in the background. + */ + public static final ConnectionMode BACKGROUND = new ConnectionMode("background"); private final String name; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java new file mode 100644 index 00000000..6c0bf270 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/DataSystemComponents.java @@ -0,0 +1,212 @@ +package com.launchdarkly.sdk.android; + +import com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder; +import com.launchdarkly.sdk.android.integrations.DataSystemBuilder; +import com.launchdarkly.sdk.android.integrations.PollingInitializerBuilder; +import com.launchdarkly.sdk.android.integrations.PollingSynchronizerBuilder; +import com.launchdarkly.sdk.android.integrations.StreamingSynchronizerBuilder; +import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.android.subsystems.ClientContext; +import com.launchdarkly.sdk.android.subsystems.Initializer; +import com.launchdarkly.sdk.android.subsystems.Synchronizer; +import com.launchdarkly.sdk.android.subsystems.TransactionalDataStore; +import com.launchdarkly.sdk.internal.http.HttpProperties; + +import java.net.URI; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; + +/** + * Factory methods for FDv2 data source components used with the + * {@link com.launchdarkly.sdk.android.integrations.DataSystemBuilder}. + *

+ * Each factory method returns a builder that implements + * {@link com.launchdarkly.sdk.android.subsystems.ComponentConfigurer} for the + * appropriate type ({@link Initializer} or {@link Synchronizer}). You may + * configure properties on the builder and then pass it to + * {@link com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder#initializers} + * or {@link com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder#synchronizers}. + *

+ * Example: + *


+ *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+ *         .mobileKey("my-key")
+ *         .dataSystem(
+ *             Components.dataSystem()
+ *                 .customizeConnectionMode(ConnectionMode.STREAMING,
+ *                     DataSystemComponents.customMode()
+ *                         .initializers(DataSystemComponents.pollingInitializer())
+ *                         .synchronizers(
+ *                             DataSystemComponents.streamingSynchronizer()
+ *                                 .initialReconnectDelayMillis(500),
+ *                             DataSystemComponents.pollingSynchronizer()
+ *                                 .pollIntervalMillis(300_000))))
+ *         .build();
+ * 
+ * + * @see com.launchdarkly.sdk.android.integrations.DataSystemBuilder + * @see com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder + */ +public abstract class DataSystemComponents { + + private DataSystemComponents() {} + + static final class PollingInitializerBuilderImpl extends PollingInitializerBuilder { + @Override + public Initializer build(ClientContext clientContext) { + ClientContextImpl impl = ClientContextImpl.get(clientContext); + SelectorSource selectorSource = makeSelectorSource(impl); + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : clientContext.getServiceEndpoints(); + URI pollingBase = StandardEndpoints.selectBaseUri( + endpoints.getPollingBaseUri(), + StandardEndpoints.DEFAULT_POLLING_BASE_URI, + "polling", clientContext.getBaseLogger()); + HttpProperties httpProps = LDUtil.makeHttpProperties(clientContext); + FDv2Requestor requestor = new DefaultFDv2Requestor( + clientContext.getEvaluationContext(), pollingBase, + StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, + StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, + httpProps, clientContext.getHttp().isUseReport(), + clientContext.isEvaluationReasons(), null, + clientContext.getBaseLogger()); + return new FDv2PollingInitializer(requestor, selectorSource, + Executors.newSingleThreadExecutor(), clientContext.getBaseLogger()); + } + } + + static final class PollingSynchronizerBuilderImpl extends PollingSynchronizerBuilder { + @Override + public Synchronizer build(ClientContext clientContext) { + ClientContextImpl impl = ClientContextImpl.get(clientContext); + SelectorSource selectorSource = makeSelectorSource(impl); + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : clientContext.getServiceEndpoints(); + URI pollingBase = StandardEndpoints.selectBaseUri( + endpoints.getPollingBaseUri(), + StandardEndpoints.DEFAULT_POLLING_BASE_URI, + "polling", clientContext.getBaseLogger()); + HttpProperties httpProps = LDUtil.makeHttpProperties(clientContext); + FDv2Requestor requestor = new DefaultFDv2Requestor( + clientContext.getEvaluationContext(), pollingBase, + StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, + StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, + httpProps, clientContext.getHttp().isUseReport(), + clientContext.isEvaluationReasons(), null, + clientContext.getBaseLogger()); + ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); + return new FDv2PollingSynchronizer(requestor, selectorSource, exec, + 0, pollIntervalMillis, clientContext.getBaseLogger()); + } + } + + static final class StreamingSynchronizerBuilderImpl extends StreamingSynchronizerBuilder { + @Override + public Synchronizer build(ClientContext clientContext) { + ClientContextImpl impl = ClientContextImpl.get(clientContext); + SelectorSource selectorSource = makeSelectorSource(impl); + ServiceEndpoints endpoints = serviceEndpointsOverride != null + ? serviceEndpointsOverride + : clientContext.getServiceEndpoints(); + URI streamBase = StandardEndpoints.selectBaseUri( + endpoints.getStreamingBaseUri(), + StandardEndpoints.DEFAULT_STREAMING_BASE_URI, + "streaming", clientContext.getBaseLogger()); + URI pollingBase = StandardEndpoints.selectBaseUri( + endpoints.getPollingBaseUri(), + StandardEndpoints.DEFAULT_POLLING_BASE_URI, + "polling", clientContext.getBaseLogger()); + HttpProperties httpProps = LDUtil.makeHttpProperties(clientContext); + FDv2Requestor requestor = new DefaultFDv2Requestor( + clientContext.getEvaluationContext(), pollingBase, + StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, + StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, + httpProps, clientContext.getHttp().isUseReport(), + clientContext.isEvaluationReasons(), null, + clientContext.getBaseLogger()); + return new FDv2StreamingSynchronizer( + clientContext.getEvaluationContext(), selectorSource, streamBase, + StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH, + requestor, + initialReconnectDelayMillis, + clientContext.isEvaluationReasons(), clientContext.getHttp().isUseReport(), + httpProps, Executors.newSingleThreadExecutor(), + clientContext.getBaseLogger(), null); + } + } + + /** + * Returns a builder for a polling initializer. + *

+ * A polling initializer makes a single poll request to obtain the initial feature + * flag data set. + * + * @return a polling initializer builder + */ + public static PollingInitializerBuilder pollingInitializer() { + return new PollingInitializerBuilderImpl(); + } + + /** + * Returns a builder for a polling synchronizer. + *

+ * A polling synchronizer periodically polls LaunchDarkly for feature flag updates. + * The poll interval can be configured via + * {@link PollingSynchronizerBuilder#pollIntervalMillis(int)}. + * + * @return a polling synchronizer builder + */ + public static PollingSynchronizerBuilder pollingSynchronizer() { + return new PollingSynchronizerBuilderImpl(); + } + + /** + * Returns a builder for a streaming synchronizer. + *

+ * A streaming synchronizer maintains a persistent connection to LaunchDarkly + * and receives real-time feature flag updates. The initial reconnect delay + * can be configured via + * {@link StreamingSynchronizerBuilder#initialReconnectDelayMillis(int)}. + * + * @return a streaming synchronizer builder + */ + public static StreamingSynchronizerBuilder streamingSynchronizer() { + return new StreamingSynchronizerBuilderImpl(); + } + + /** + * Returns a builder for configuring a custom data pipeline for a connection mode. + *

+ * Use this to specify which initializers and synchronizers should run when the + * SDK is operating in a particular {@link ConnectionMode}. Pass the result to + * {@link DataSystemBuilder#customizeConnectionMode(ConnectionMode, ConnectionModeBuilder)}. + *


+     *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+     *         .mobileKey("my-key")
+     *         .dataSystem(
+     *             Components.dataSystem()
+     *                 .customizeConnectionMode(ConnectionMode.BACKGROUND,
+     *                     DataSystemComponents.customMode()
+     *                         .synchronizers(
+     *                             DataSystemComponents.pollingSynchronizer()
+     *                                 .pollIntervalMillis(21_600_000))))
+     *         .build();
+     * 
+ * + * @return a builder for configuring a custom connection mode pipeline + * @see ConnectionModeBuilder + * @see DataSystemBuilder#customizeConnectionMode(ConnectionMode, ConnectionModeBuilder) + */ + public static ConnectionModeBuilder customMode() { + return new ConnectionModeBuilder(); + } + + private static SelectorSource makeSelectorSource(ClientContextImpl impl) { + TransactionalDataStore store = impl.getTransactionalDataStore(); + return store != null + ? new SelectorSourceFacade(store) + : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java index 87bf7118..52ca9719 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/FDv2DataSourceBuilder.java @@ -9,14 +9,8 @@ import com.launchdarkly.sdk.android.subsystems.DataSourceUpdateSinkV2; import com.launchdarkly.sdk.android.subsystems.Initializer; import com.launchdarkly.sdk.android.subsystems.Synchronizer; -import com.launchdarkly.sdk.android.integrations.PollingDataSourceBuilder; -import com.launchdarkly.sdk.android.integrations.StreamingDataSourceBuilder; -import com.launchdarkly.sdk.android.subsystems.TransactionalDataStore; -import com.launchdarkly.sdk.internal.http.HttpProperties; -import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; @@ -25,10 +19,10 @@ import java.util.concurrent.ScheduledExecutorService; /** - * Builds an {@link FDv2DataSource} by resolving {@link ComponentConfigurer} factories - * into zero-arg {@link FDv2DataSource.DataSourceFactory} instances. The builder is the - * sole owner of mode resolution; {@link ConnectivityManager} configures the target mode - * via {@link #setActiveMode} before calling the standard {@link #build}. + * Builds an {@link FDv2DataSource} and resolves the mode table from + * {@link ComponentConfigurer} factories into zero-arg {@link FDv2DataSource.DataSourceFactory} + * instances. The resolved table is stored and exposed via {@link #getResolvedModeTable()} + * so that {@link ConnectivityManager} can perform mode-to-definition lookups when switching modes. *

* Package-private — not part of the public SDK API. */ @@ -36,142 +30,61 @@ class FDv2DataSourceBuilder implements ComponentConfigurer { private final Map modeTable; private final ConnectionMode startingMode; + private final ModeResolutionTable resolutionTable; + private final boolean automaticModeSwitching; private ConnectionMode activeMode; private boolean includeInitializers = true; // false during mode switches to skip initializers (CONNMODE 2.0.1) private ScheduledExecutorService sharedExecutor; FDv2DataSourceBuilder() { - this(makeDefaultModeTable(), ConnectionMode.STREAMING); + this(makeDefaultModeTable(), ConnectionMode.STREAMING, ModeResolutionTable.MOBILE, true); } - private static Map makeDefaultModeTable() { - ComponentConfigurer pollingInitializer = ctx -> { - ClientContextImpl impl = ClientContextImpl.get(ctx); - TransactionalDataStore store = impl.getTransactionalDataStore(); - SelectorSource selectorSource = store != null - ? new SelectorSourceFacade(store) - : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; - URI pollingBase = StandardEndpoints.selectBaseUri( - ctx.getServiceEndpoints().getPollingBaseUri(), - StandardEndpoints.DEFAULT_POLLING_BASE_URI, - "polling", ctx.getBaseLogger()); - HttpProperties httpProps = LDUtil.makeHttpProperties(ctx); - FDv2Requestor requestor = new DefaultFDv2Requestor( - ctx.getEvaluationContext(), pollingBase, - StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, - StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, - httpProps, ctx.getHttp().isUseReport(), - ctx.isEvaluationReasons(), null, ctx.getBaseLogger()); - return new FDv2PollingInitializer(requestor, selectorSource, - Executors.newSingleThreadExecutor(), ctx.getBaseLogger()); - }; - - ComponentConfigurer pollingSynchronizer = ctx -> { - ClientContextImpl impl = ClientContextImpl.get(ctx); - TransactionalDataStore store = impl.getTransactionalDataStore(); - SelectorSource selectorSource = store != null - ? new SelectorSourceFacade(store) - : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; - URI pollingBase = StandardEndpoints.selectBaseUri( - ctx.getServiceEndpoints().getPollingBaseUri(), - StandardEndpoints.DEFAULT_POLLING_BASE_URI, - "polling", ctx.getBaseLogger()); - HttpProperties httpProps = LDUtil.makeHttpProperties(ctx); - FDv2Requestor requestor = new DefaultFDv2Requestor( - ctx.getEvaluationContext(), pollingBase, - StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, - StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, - httpProps, ctx.getHttp().isUseReport(), - ctx.isEvaluationReasons(), null, ctx.getBaseLogger()); - ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); - return new FDv2PollingSynchronizer(requestor, selectorSource, exec, - 0, PollingDataSourceBuilder.DEFAULT_POLL_INTERVAL_MILLIS, ctx.getBaseLogger()); - }; - - ComponentConfigurer streamingSynchronizer = ctx -> { - ClientContextImpl impl = ClientContextImpl.get(ctx); - TransactionalDataStore store = impl.getTransactionalDataStore(); - SelectorSource selectorSource = store != null - ? new SelectorSourceFacade(store) - : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; - URI streamBase = StandardEndpoints.selectBaseUri( - ctx.getServiceEndpoints().getStreamingBaseUri(), - StandardEndpoints.DEFAULT_STREAMING_BASE_URI, - "streaming", ctx.getBaseLogger()); - URI pollingBase = StandardEndpoints.selectBaseUri( - ctx.getServiceEndpoints().getPollingBaseUri(), - StandardEndpoints.DEFAULT_POLLING_BASE_URI, - "polling", ctx.getBaseLogger()); - HttpProperties httpProps = LDUtil.makeHttpProperties(ctx); - FDv2Requestor requestor = new DefaultFDv2Requestor( - ctx.getEvaluationContext(), pollingBase, - StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, - StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, - httpProps, ctx.getHttp().isUseReport(), - ctx.isEvaluationReasons(), null, ctx.getBaseLogger()); - return new FDv2StreamingSynchronizer( - ctx.getEvaluationContext(), selectorSource, streamBase, - StandardEndpoints.FDV2_STREAMING_REQUEST_BASE_PATH, - requestor, - StreamingDataSourceBuilder.DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS, - ctx.isEvaluationReasons(), ctx.getHttp().isUseReport(), - httpProps, Executors.newSingleThreadExecutor(), - ctx.getBaseLogger(), null); - }; - - ComponentConfigurer backgroundPollingSynchronizer = ctx -> { - ClientContextImpl impl = ClientContextImpl.get(ctx); - TransactionalDataStore store = impl.getTransactionalDataStore(); - SelectorSource selectorSource = store != null - ? new SelectorSourceFacade(store) - : () -> com.launchdarkly.sdk.fdv2.Selector.EMPTY; - URI pollingBase = StandardEndpoints.selectBaseUri( - ctx.getServiceEndpoints().getPollingBaseUri(), - StandardEndpoints.DEFAULT_POLLING_BASE_URI, - "polling", ctx.getBaseLogger()); - HttpProperties httpProps = LDUtil.makeHttpProperties(ctx); - FDv2Requestor requestor = new DefaultFDv2Requestor( - ctx.getEvaluationContext(), pollingBase, - StandardEndpoints.FDV2_POLLING_REQUEST_GET_BASE_PATH, - StandardEndpoints.FDV2_POLLING_REQUEST_REPORT_BASE_PATH, - httpProps, ctx.getHttp().isUseReport(), - ctx.isEvaluationReasons(), null, ctx.getBaseLogger()); - ScheduledExecutorService exec = Executors.newScheduledThreadPool(1); - return new FDv2PollingSynchronizer(requestor, selectorSource, exec, - 0, LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS, ctx.getBaseLogger()); - }; - - Map table = new LinkedHashMap<>(); - table.put(ConnectionMode.STREAMING, new ModeDefinition( - Arrays.asList(pollingInitializer, pollingInitializer), - Arrays.asList(streamingSynchronizer, pollingSynchronizer) - )); - table.put(ConnectionMode.POLLING, new ModeDefinition( - Collections.singletonList(pollingInitializer), - Collections.singletonList(pollingSynchronizer) - )); - table.put(ConnectionMode.OFFLINE, new ModeDefinition( - Collections.singletonList(pollingInitializer), - Collections.>emptyList() - )); - table.put(ConnectionMode.ONE_SHOT, new ModeDefinition( - Arrays.asList(pollingInitializer, pollingInitializer, pollingInitializer), - Collections.>emptyList() - )); - table.put(ConnectionMode.BACKGROUND, new ModeDefinition( - Collections.singletonList(pollingInitializer), - Collections.singletonList(backgroundPollingSynchronizer) - )); - return table; + FDv2DataSourceBuilder( + @NonNull Map modeTable, + @NonNull ConnectionMode startingMode + ) { + this(modeTable, startingMode, ModeResolutionTable.MOBILE, true); } FDv2DataSourceBuilder( @NonNull Map modeTable, - @NonNull ConnectionMode startingMode + @NonNull ConnectionMode startingMode, + @NonNull ModeResolutionTable resolutionTable + ) { + this(modeTable, startingMode, resolutionTable, true); + } + + FDv2DataSourceBuilder( + @NonNull Map modeTable, + @NonNull ConnectionMode startingMode, + @NonNull ModeResolutionTable resolutionTable, + boolean automaticModeSwitching ) { this.modeTable = Collections.unmodifiableMap(new LinkedHashMap<>(modeTable)); this.startingMode = startingMode; + this.resolutionTable = resolutionTable; + this.automaticModeSwitching = automaticModeSwitching; + } + + /** + * Returns the mode resolution table used to map platform state to connection modes. + * + * @return the resolution table + */ + @NonNull + ModeResolutionTable getResolutionTable() { + return resolutionTable; + } + + /** + * Returns whether automatic mode switching is enabled. + * + * @return true if automatic mode switching is enabled + */ + boolean isAutomaticModeSwitching() { + return automaticModeSwitching; } @NonNull @@ -251,4 +164,9 @@ private static ResolvedModeDefinition resolve( } return new ResolvedModeDefinition(initFactories, syncFactories); } + + private static Map makeDefaultModeTable() { + return new com.launchdarkly.sdk.android.integrations.DataSystemBuilder() + .buildModeTable(false); + } } 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 42b827eb..7422e404 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 @@ -8,6 +8,7 @@ import com.launchdarkly.sdk.ContextKind; import com.launchdarkly.sdk.LDContext; import com.launchdarkly.sdk.android.integrations.ApplicationInfoBuilder; +import com.launchdarkly.sdk.android.integrations.DataSystemBuilder; import com.launchdarkly.sdk.android.integrations.HooksConfigurationBuilder; import com.launchdarkly.sdk.android.integrations.PluginsConfigurationBuilder; import com.launchdarkly.sdk.android.integrations.ServiceEndpointsBuilder; @@ -228,6 +229,7 @@ public enum AutoEnvAttributes { private ApplicationInfoBuilder applicationInfoBuilder = null; private ComponentConfigurer dataSource = null; + private DataSystemBuilder dataSystemBuilder = null; private ComponentConfigurer events = null; private HooksConfigurationBuilder hooksConfigurationBuilder = null; private PluginsConfigurationBuilder pluginsConfigurationBuilder = null; @@ -383,6 +385,43 @@ public Builder applicationInfo(ApplicationInfoBuilder applicationInfoBuilder) { */ public Builder dataSource(ComponentConfigurer dataSourceConfigurer) { this.dataSource = dataSourceConfigurer; + this.dataSystemBuilder = null; + return this; + } + + /** + * Configures the SDK's data system, which controls how the SDK acquires and + * maintains feature flag data using the FDv2 protocol. + *

+ * The data system supports per-mode customization of initializers and synchronizers + * for foreground, background, and other platform states. This is the recommended + * way to configure FDv2 data sources. + *

+ * This is mutually exclusive with {@link #dataSource(ComponentConfigurer)}. If both + * are called, the last one wins. + *


+         *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+         *         .mobileKey("my-key")
+         *         .dataSystem(
+         *             Components.dataSystem()
+         *                 .customizeConnectionMode(ConnectionMode.STREAMING,
+ *                     DataSystemComponents.customMode()
+ *                         .initializers(DataSystemComponents.pollingInitializer())
+         *                         .synchronizers(
+         *                             DataSystemComponents.streamingSynchronizer()
+         *                                 .initialReconnectDelayMillis(500),
+         *                             DataSystemComponents.pollingSynchronizer())))
+         *         .build();
+         * 
+ * + * @param dataSystemBuilder the data system configuration builder + * @return the main configuration builder + * @see Components#dataSystem() + * @see DataSystemBuilder + */ + public Builder dataSystem(DataSystemBuilder dataSystemBuilder) { + this.dataSystemBuilder = dataSystemBuilder; + this.dataSource = null; return this; } @@ -722,11 +761,27 @@ public LDConfig build() { null : applicationInfoBuilder.createApplicationInfo(); + ComponentConfigurer effectiveDataSource; + if (this.dataSystemBuilder != null) { + Map modeTable = + this.dataSystemBuilder.buildModeTable(disableBackgroundUpdating); + ConnectionMode startingMode = this.dataSystemBuilder.getForegroundConnectionMode(); + ConnectionMode backgroundMode = this.dataSystemBuilder.getBackgroundConnectionMode(); + ModeResolutionTable resolutionTable = ModeResolutionTable.createMobile( + startingMode, backgroundMode); + boolean autoSwitch = this.dataSystemBuilder.isAutomaticModeSwitching(); + effectiveDataSource = new FDv2DataSourceBuilder( + modeTable, startingMode, resolutionTable, autoSwitch); + } else { + effectiveDataSource = this.dataSource == null + ? Components.streamingDataSource() : this.dataSource; + } + return new LDConfig( mobileKeys, serviceEndpoints, applicationInfo, - this.dataSource == null ? Components.streamingDataSource() : this.dataSource, + effectiveDataSource, this.events == null ? Components.sendEvents() : this.events, (this.hooksConfigurationBuilder == null ? Components.hooks() : this.hooksConfigurationBuilder).build(), (this.pluginsConfigurationBuilder == null ? Components.plugins() : this.pluginsConfigurationBuilder).build(), diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeDefinition.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeDefinition.java index 81d69b8f..1330b322 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeDefinition.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeDefinition.java @@ -17,18 +17,22 @@ * At build time, {@code FDv2DataSourceBuilder} resolves each {@link ComponentConfigurer} * into a {@link FDv2DataSource.DataSourceFactory} by partially applying the * {@link com.launchdarkly.sdk.android.subsystems.ClientContext}. - *

- * Package-private — not part of the public SDK API. * * @see ConnectionMode * @see ResolvedModeDefinition */ -final class ModeDefinition { +public final class ModeDefinition { private final List> initializers; private final List> synchronizers; - ModeDefinition( + /** + * Constructs a mode definition with the given initializers and synchronizers. + * + * @param initializers the initializer configurers, in priority order + * @param synchronizers the synchronizer configurers, in priority order + */ + public ModeDefinition( @NonNull List> initializers, @NonNull List> synchronizers ) { @@ -36,13 +40,23 @@ final class ModeDefinition { this.synchronizers = Collections.unmodifiableList(synchronizers); } + /** + * Returns the initializer configurers for this mode. + * + * @return an unmodifiable list of initializer configurers + */ @NonNull - List> getInitializers() { + public List> getInitializers() { return initializers; } + /** + * Returns the synchronizer configurers for this mode. + * + * @return an unmodifiable list of synchronizer configurers + */ @NonNull - List> getSynchronizers() { + public List> getSynchronizers() { return synchronizers; } } diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeResolutionTable.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeResolutionTable.java index f6b1817a..1d6dcb67 100644 --- a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeResolutionTable.java +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ModeResolutionTable.java @@ -24,17 +24,38 @@ */ final class ModeResolutionTable { - static final ModeResolutionTable MOBILE = new ModeResolutionTable(Arrays.asList( - new ModeResolutionEntry( - state -> !state.isNetworkAvailable(), - ConnectionMode.OFFLINE), - new ModeResolutionEntry( - state -> !state.isForeground(), - ConnectionMode.BACKGROUND), - new ModeResolutionEntry( - state -> true, - ConnectionMode.STREAMING) - )); + static final ModeResolutionTable MOBILE = createMobile( + ConnectionMode.STREAMING, ConnectionMode.BACKGROUND); + + /** + * Creates a mobile resolution table with configurable foreground and background modes. + * The resolution order is: + *

    + *
  1. No network → OFFLINE
  2. + *
  3. Background → {@code backgroundMode}
  4. + *
  5. Foreground (catch-all) → {@code foregroundMode}
  6. + *
+ * + * @param foregroundMode the mode to use when in the foreground + * @param backgroundMode the mode to use when in the background + * @return a new resolution table + */ + static ModeResolutionTable createMobile( + ConnectionMode foregroundMode, + ConnectionMode backgroundMode + ) { + return new ModeResolutionTable(Arrays.asList( + new ModeResolutionEntry( + state -> !state.isNetworkAvailable(), + ConnectionMode.OFFLINE), + new ModeResolutionEntry( + state -> !state.isForeground(), + backgroundMode), + new ModeResolutionEntry( + state -> true, + foregroundMode) + )); + } private final List entries; diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java new file mode 100644 index 00000000..97a2c128 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/ConnectionModeBuilder.java @@ -0,0 +1,112 @@ +package com.launchdarkly.sdk.android.integrations; + +import androidx.annotation.NonNull; + +import com.launchdarkly.sdk.android.ConnectionMode; +import com.launchdarkly.sdk.android.DataSystemComponents; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.Initializer; +import com.launchdarkly.sdk.android.subsystems.Synchronizer; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Configures the data pipeline (initializers and synchronizers) for a single + * {@link ConnectionMode}. + *

+ * Initializers are one-shot data sources that run in order at startup to + * obtain an initial set of feature flag data. The SDK tries each initializer in + * sequence until one succeeds. + *

+ * Synchronizers are long-lived data sources that keep the feature flag data + * up to date after initialization. The SDK uses the first synchronizer and falls + * back to subsequent ones if it encounters errors. + *

+ * Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#customMode()}, + * configure it, and pass it to + * {@link DataSystemBuilder#customizeConnectionMode(ConnectionMode, ConnectionModeBuilder)}: + *


+ *     DataSystemComponents.customMode()
+ *         .initializers(DataSystemComponents.pollingInitializer())
+ *         .synchronizers(
+ *             DataSystemComponents.streamingSynchronizer(),
+ *             DataSystemComponents.pollingSynchronizer())
+ * 
+ * + * @see DataSystemBuilder + * @see DataSystemComponents + */ +public class ConnectionModeBuilder { + + private final List> initializers = new ArrayList<>(); + private final List> synchronizers = new ArrayList<>(); + + /** + * Sets the initializers for this connection mode. + *

+ * Initializers run in order. The SDK advances to the next initializer if one + * fails or returns partial data. Any previously configured initializers are + * replaced. + *

+ * Use factory methods in {@link DataSystemComponents} to obtain builder instances: + *


+     *     builder.initializers(DataSystemComponents.pollingInitializer())
+     * 
+ * + * @param initializers the initializer configurers, in priority order + * @return this builder + */ + @SafeVarargs + public final ConnectionModeBuilder initializers(@NonNull ComponentConfigurer... initializers) { + this.initializers.clear(); + this.initializers.addAll(Arrays.asList(initializers)); + return this; + } + + /** + * Sets the synchronizers for this connection mode. + *

+ * Synchronizers keep data up to date after initialization. The SDK uses the + * first synchronizer and falls back to subsequent ones on error. Any previously + * configured synchronizers are replaced. + *

+ * Use factory methods in {@link DataSystemComponents} to obtain builder instances: + *


+     *     builder.synchronizers(
+     *         DataSystemComponents.streamingSynchronizer(),
+     *         DataSystemComponents.pollingSynchronizer())
+     * 
+ * + * @param synchronizers the synchronizer configurers, in priority order + * @return this builder + */ + @SafeVarargs + public final ConnectionModeBuilder synchronizers(@NonNull ComponentConfigurer... synchronizers) { + this.synchronizers.clear(); + this.synchronizers.addAll(Arrays.asList(synchronizers)); + return this; + } + + /** + * Returns the configured initializers as an unmodifiable list. + * + * @return the initializer configurers + */ + @NonNull + public List> getInitializers() { + return Collections.unmodifiableList(initializers); + } + + /** + * Returns the configured synchronizers as an unmodifiable list. + * + * @return the synchronizer configurers + */ + @NonNull + public List> getSynchronizers() { + return Collections.unmodifiableList(synchronizers); + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java new file mode 100644 index 00000000..f2ff81ba --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/DataSystemBuilder.java @@ -0,0 +1,274 @@ +package com.launchdarkly.sdk.android.integrations; + +import androidx.annotation.NonNull; + +import com.launchdarkly.sdk.android.ConnectionMode; +import com.launchdarkly.sdk.android.DataSystemComponents; +import com.launchdarkly.sdk.android.LDConfig; +import com.launchdarkly.sdk.android.ModeDefinition; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.Initializer; +import com.launchdarkly.sdk.android.subsystems.Synchronizer; + +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Configures the SDK's data system: how and when the SDK acquires feature flag + * data across different platform states (foreground, background, offline, etc.). + *

+ * The data system is organized around {@link ConnectionMode connection modes}. Each + * mode has a data pipeline consisting of initializers (one-shot data loads) + * and synchronizers (ongoing data updates). The SDK automatically transitions + * between modes based on platform state (foreground/background, network availability). + *

+ * Quick start — use defaults: + *


+ *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+ *         .mobileKey("my-key")
+ *         .dataSystem(Components.dataSystem())
+ *         .build();
+ * 
+ *

+ * Custom mode pipelines — background polling once every 6 hours: + *


+ *     LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+ *         .mobileKey("my-key")
+ *         .dataSystem(
+ *             Components.dataSystem()
+ *                 .customizeConnectionMode(ConnectionMode.BACKGROUND,
+ *                     DataSystemComponents.customMode()
+ *                         .initializers(DataSystemComponents.pollingInitializer())
+ *                         .synchronizers(
+ *                             DataSystemComponents.pollingSynchronizer()
+ *                                 .pollIntervalMillis(21_600_000))))
+ *         .build();
+ * 
+ *

+ * Change the foreground mode to polling: + *


+ *     Components.dataSystem()
+ *         .foregroundConnectionMode(ConnectionMode.POLLING)
+ * 
+ *

+ * Disable automatic mode switching: + *


+ *     Components.dataSystem()
+ *         .automaticModeSwitching(false)
+ *         .foregroundConnectionMode(ConnectionMode.STREAMING)
+ * 
+ * When automatic mode switching is disabled, the SDK stays in the + * {@link #foregroundConnectionMode foreground connection mode} and does not react to + * platform state changes (foreground/background, network availability). This can be + * useful when you want full control over which mode the SDK uses. + *

+ * Obtain an instance from {@link com.launchdarkly.sdk.android.Components#dataSystem()}. + * + * @see ConnectionMode + * @see ConnectionModeBuilder + * @see DataSystemComponents + */ +public class DataSystemBuilder { + + private ConnectionMode foregroundConnectionMode = ConnectionMode.STREAMING; + private ConnectionMode backgroundConnectionMode = ConnectionMode.BACKGROUND; + private boolean automaticModeSwitching = true; + private final Map connectionModeOverrides = new LinkedHashMap<>(); + + /** + * Sets the connection mode used when the application is in the foreground. + *

+ * This determines which entry in the mode table is used when the SDK resolves + * the foreground platform state. For instance, setting this to + * {@link ConnectionMode#POLLING} means the SDK will use the polling mode + * pipeline when the app is in the foreground. + *

+ * The default is {@link ConnectionMode#STREAMING}. + * + * @param mode the foreground connection mode + * @return this builder + */ + public DataSystemBuilder foregroundConnectionMode(@NonNull ConnectionMode mode) { + this.foregroundConnectionMode = mode; + return this; + } + + /** + * Sets the connection mode used when the application is in the background. + *

+ * The default is {@link ConnectionMode#BACKGROUND}. + * + * @param mode the background connection mode + * @return this builder + */ + public DataSystemBuilder backgroundConnectionMode(@NonNull ConnectionMode mode) { + this.backgroundConnectionMode = mode; + return this; + } + + /** + * Enables or disables automatic mode switching based on platform state. + *

+ * When enabled (the default), the SDK automatically transitions between connection + * modes as the platform state changes (e.g., foreground to background, network loss). + *

+ * When disabled, the SDK stays in the {@link #foregroundConnectionMode foreground connection + * mode} for its entire lifecycle and ignores platform state changes. This is useful + * when you want explicit control over data acquisition behavior regardless of whether + * the app is foregrounded, backgrounded, or experiencing network changes. + *

+ * Note that {@link com.launchdarkly.sdk.android.LDClient#setForceOffline(boolean)} + * still works independently of this setting. + * + * @param enabled true to enable automatic mode switching (default), false to disable + * @return this builder + */ + public DataSystemBuilder automaticModeSwitching(boolean enabled) { + this.automaticModeSwitching = enabled; + return this; + } + + /** + * Overrides the data pipeline for a specific connection mode. + *

+ * This only affects the specified mode. All other connection modes that are not + * customized continue to use their default pipelines. For example, customizing + * {@link ConnectionMode#BACKGROUND} does not change the behavior of + * {@link ConnectionMode#STREAMING} or any other mode. + *

+ * Example — set background polling to once every 6 hours: + *


+     *     Components.dataSystem()
+     *         .customizeConnectionMode(ConnectionMode.BACKGROUND,
+     *             DataSystemComponents.customMode()
+     *                 .initializers(DataSystemComponents.pollingInitializer())
+     *                 .synchronizers(
+     *                     DataSystemComponents.pollingSynchronizer()
+     *                         .pollIntervalMillis(21_600_000)))
+     * 
+ * + * @param mode the connection mode to customize + * @param builder the pipeline configuration for this mode + * @return this builder + */ + public DataSystemBuilder customizeConnectionMode( + @NonNull ConnectionMode mode, + @NonNull ConnectionModeBuilder builder + ) { + connectionModeOverrides.put(mode, builder); + return this; + } + + /** + * Returns the configured foreground connection mode. + * + * @return the foreground connection mode + */ + @NonNull + public ConnectionMode getForegroundConnectionMode() { + return foregroundConnectionMode; + } + + /** + * Returns the configured background connection mode. + * + * @return the background connection mode + */ + @NonNull + public ConnectionMode getBackgroundConnectionMode() { + return backgroundConnectionMode; + } + + /** + * Returns whether automatic mode switching is enabled. + * + * @return true if automatic mode switching is enabled + */ + public boolean isAutomaticModeSwitching() { + return automaticModeSwitching; + } + + /** + * Returns any user-specified mode overrides. + * + * @return an unmodifiable map of overridden connection modes + */ + @NonNull + public Map getConnectionModeOverrides() { + return Collections.unmodifiableMap(connectionModeOverrides); + } + + /** + * Builds the full mode table by starting with defaults and applying any user + * overrides and LDConfig-level settings. + *

+ * If {@code disableBackgroundUpdating} is true, the background mode entry + * is replaced with an empty pipeline (no initializers or synchronizers). + * + * @param disableBackgroundUpdating whether background updates are disabled + * @return the complete mode table + */ + @NonNull + public Map buildModeTable(boolean disableBackgroundUpdating) { + Map table = makeDefaultModeTable(); + + for (Map.Entry entry : connectionModeOverrides.entrySet()) { + ConnectionModeBuilder cmb = entry.getValue(); + table.put(entry.getKey(), new ModeDefinition( + cmb.getInitializers(), + cmb.getSynchronizers() + )); + } + + if (disableBackgroundUpdating) { + table.put(ConnectionMode.BACKGROUND, new ModeDefinition( + Collections.>emptyList(), + Collections.>emptyList() + )); + } + + return table; + } + + /** + * Produces the default mode table. This matches the pipelines previously + * hardcoded in {@code FDv2DataSourceBuilder.makeDefaultModeTable()}. + */ + @NonNull + private static Map makeDefaultModeTable() { + ComponentConfigurer pollingInitializer = DataSystemComponents.pollingInitializer(); + + ComponentConfigurer pollingSynchronizer = DataSystemComponents.pollingSynchronizer(); + + ComponentConfigurer streamingSynchronizer = DataSystemComponents.streamingSynchronizer(); + + ComponentConfigurer backgroundPollingSynchronizer = + DataSystemComponents.pollingSynchronizer() + .pollIntervalMillis(LDConfig.DEFAULT_BACKGROUND_POLL_INTERVAL_MILLIS); + + Map table = new LinkedHashMap<>(); + table.put(ConnectionMode.STREAMING, new ModeDefinition( + Arrays.asList(pollingInitializer, pollingInitializer), + Arrays.asList(streamingSynchronizer, pollingSynchronizer) + )); + table.put(ConnectionMode.POLLING, new ModeDefinition( + Collections.singletonList(pollingInitializer), + Collections.singletonList(pollingSynchronizer) + )); + table.put(ConnectionMode.OFFLINE, new ModeDefinition( + Collections.singletonList(pollingInitializer), + Collections.>emptyList() + )); + table.put(ConnectionMode.ONE_SHOT, new ModeDefinition( + Arrays.asList(pollingInitializer, pollingInitializer, pollingInitializer), + Collections.>emptyList() + )); + table.put(ConnectionMode.BACKGROUND, new ModeDefinition( + Collections.singletonList(pollingInitializer), + Collections.>singletonList(backgroundPollingSynchronizer) + )); + return table; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingInitializerBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingInitializerBuilder.java new file mode 100644 index 00000000..26e8e2db --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingInitializerBuilder.java @@ -0,0 +1,51 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.Initializer; + +/** + * Contains methods for configuring the polling initializer. + *

+ * A polling initializer makes a single poll request to retrieve the initial set + * of feature flag data. It is typically used as the first step in a connection mode's + * data pipeline. + *

+ * Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#pollingInitializer()}, + * configure it, and pass it to + * {@link ConnectionModeBuilder#initializers(ComponentConfigurer[])}: + *


+ *     DataSystemComponents.customMode()
+ *         .initializers(DataSystemComponents.pollingInitializer())
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling + * {@link com.launchdarkly.sdk.android.DataSystemComponents#pollingInitializer()}. + * + * @see com.launchdarkly.sdk.android.DataSystemComponents + * @see ConnectionModeBuilder + */ +public abstract class PollingInitializerBuilder implements ComponentConfigurer { + + /** + * Per-source service endpoint override, or null to use the SDK-level endpoints. + */ + protected ServiceEndpoints serviceEndpointsOverride; + + /** + * Sets overrides for the service endpoints used by this initializer. + *

+ * In typical usage, the initializer uses the service endpoints configured at the + * SDK level via + * {@link com.launchdarkly.sdk.android.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * Use this method only when you need a specific initializer to connect to different + * endpoints than the rest of the SDK. + * + * @param serviceEndpointsBuilder the service endpoints override + * @return this builder + */ + public PollingInitializerBuilder serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsBuilder) { + this.serviceEndpointsOverride = serviceEndpointsBuilder.createServiceEndpoints(); + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingSynchronizerBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingSynchronizerBuilder.java new file mode 100644 index 00000000..623fc238 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/PollingSynchronizerBuilder.java @@ -0,0 +1,77 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.Synchronizer; + +/** + * Contains methods for configuring the polling synchronizer. + *

+ * A polling synchronizer periodically polls LaunchDarkly for feature flag updates. + * It can be used as a primary synchronizer or as a fallback when the streaming + * connection is unavailable. + *

+ * Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#pollingSynchronizer()}, + * configure it, and pass it to + * {@link ConnectionModeBuilder#synchronizers(ComponentConfigurer[])}: + *


+ *     DataSystemComponents.customMode()
+ *         .synchronizers(
+ *             DataSystemComponents.pollingSynchronizer()
+ *                 .pollIntervalMillis(60_000))
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling + * {@link com.launchdarkly.sdk.android.DataSystemComponents#pollingSynchronizer()}. + * + * @see com.launchdarkly.sdk.android.DataSystemComponents + * @see ConnectionModeBuilder + */ +public abstract class PollingSynchronizerBuilder implements ComponentConfigurer { + + /** + * The default value for {@link #pollIntervalMillis(int)}: 5 minutes (300,000 ms). + */ + public static final int DEFAULT_POLL_INTERVAL_MILLIS = 300_000; + + /** + * The polling interval in milliseconds. + */ + protected int pollIntervalMillis = DEFAULT_POLL_INTERVAL_MILLIS; + + /** + * Per-source service endpoint override, or null to use the SDK-level endpoints. + */ + protected ServiceEndpoints serviceEndpointsOverride; + + /** + * Sets the interval at which the SDK will poll for feature flag updates. + *

+ * The default and minimum value is {@link #DEFAULT_POLL_INTERVAL_MILLIS}. Values + * less than this will be set to the default. + * + * @param pollIntervalMillis the polling interval in milliseconds + * @return this builder + */ + public PollingSynchronizerBuilder pollIntervalMillis(int pollIntervalMillis) { + this.pollIntervalMillis = Math.max(pollIntervalMillis, DEFAULT_POLL_INTERVAL_MILLIS); + return this; + } + + /** + * Sets overrides for the service endpoints used by this synchronizer. + *

+ * In typical usage, the synchronizer uses the service endpoints configured at the + * SDK level via + * {@link com.launchdarkly.sdk.android.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * Use this method only when you need a specific synchronizer to connect to different + * endpoints than the rest of the SDK. + * + * @param serviceEndpointsBuilder the service endpoints override + * @return this builder + */ + public PollingSynchronizerBuilder serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsBuilder) { + this.serviceEndpointsOverride = serviceEndpointsBuilder.createServiceEndpoints(); + return this; + } +} diff --git a/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingSynchronizerBuilder.java b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingSynchronizerBuilder.java new file mode 100644 index 00000000..b0a4b792 --- /dev/null +++ b/launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/integrations/StreamingSynchronizerBuilder.java @@ -0,0 +1,83 @@ +package com.launchdarkly.sdk.android.integrations; + +import com.launchdarkly.sdk.android.interfaces.ServiceEndpoints; +import com.launchdarkly.sdk.android.subsystems.ComponentConfigurer; +import com.launchdarkly.sdk.android.subsystems.Synchronizer; + +/** + * Contains methods for configuring the streaming synchronizer. + *

+ * A streaming synchronizer maintains a persistent connection to LaunchDarkly's + * Flag Delivery service and receives real-time feature flag updates. It is + * typically the primary synchronizer for the foreground streaming mode. + *

+ * Obtain an instance from {@link com.launchdarkly.sdk.android.DataSystemComponents#streamingSynchronizer()}, + * configure it, and pass it to + * {@link ConnectionModeBuilder#synchronizers(ComponentConfigurer[])}: + *


+ *     DataSystemComponents.customMode()
+ *         .synchronizers(
+ *             DataSystemComponents.streamingSynchronizer()
+ *                 .initialReconnectDelayMillis(500))
+ * 
+ *

+ * Note that this class is abstract; the actual implementation is created by calling + * {@link com.launchdarkly.sdk.android.DataSystemComponents#streamingSynchronizer()}. + * + * @see com.launchdarkly.sdk.android.DataSystemComponents + * @see ConnectionModeBuilder + */ +public abstract class StreamingSynchronizerBuilder implements ComponentConfigurer { + + /** + * The default value for {@link #initialReconnectDelayMillis(int)}: 1000 milliseconds. + */ + public static final int DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS = 1_000; + + /** + * The initial reconnection delay in milliseconds. + */ + protected int initialReconnectDelayMillis = DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS; + + /** + * Per-source service endpoint override, or null to use the SDK-level endpoints. + */ + protected ServiceEndpoints serviceEndpointsOverride; + + /** + * 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 this builder + */ + public StreamingSynchronizerBuilder initialReconnectDelayMillis(int initialReconnectDelayMillis) { + this.initialReconnectDelayMillis = initialReconnectDelayMillis <= 0 + ? DEFAULT_INITIAL_RECONNECT_DELAY_MILLIS + : initialReconnectDelayMillis; + return this; + } + + /** + * Sets overrides for the service endpoints used by this synchronizer. + *

+ * In typical usage, the synchronizer uses the service endpoints configured at the + * SDK level via + * {@link com.launchdarkly.sdk.android.LDConfig.Builder#serviceEndpoints(ServiceEndpointsBuilder)}. + * Use this method only when you need a specific synchronizer to connect to different + * endpoints than the rest of the SDK. + * + * @param serviceEndpointsBuilder the service endpoints override + * @return this builder + */ + public StreamingSynchronizerBuilder serviceEndpointsOverride(ServiceEndpointsBuilder serviceEndpointsBuilder) { + this.serviceEndpointsOverride = serviceEndpointsBuilder.createServiceEndpoints(); + return this; + } +}