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: + *
+ * 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
+ * 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.
+ *
- * Package-private — not part of the public SDK API.
*
* @see ConnectionMode
* @see ResolvedModeDefinition
*/
-final class ModeDefinition {
+public final class ModeDefinition {
private final List
+ * 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)}:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * 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:
+ *
+ * Custom mode pipelines — background polling once every 6 hours:
+ *
+ * Change the foreground mode to polling:
+ *
+ * Disable automatic mode switching:
+ *
+ * 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
+ * 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:
+ *
+ * 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
+ * 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[])}:
+ *
+ * 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
+ * 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[])}:
+ *
+ * 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 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[])}:
+ *
+ * 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 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;
+ }
+}
+ *
+ * @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
+ * 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 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
+ *
+ * @see DataSystemBuilder
+ * @see DataSystemComponents
+ */
+public class ConnectionModeBuilder {
+
+ private final List
+ * DataSystemComponents.customMode()
+ * .initializers(DataSystemComponents.pollingInitializer())
+ * .synchronizers(
+ * DataSystemComponents.streamingSynchronizer(),
+ * DataSystemComponents.pollingSynchronizer())
+ *
+ *
+ * @param initializers the initializer configurers, in priority order
+ * @return this builder
+ */
+ @SafeVarargs
+ public final ConnectionModeBuilder initializers(@NonNull ComponentConfigurer
+ * builder.initializers(DataSystemComponents.pollingInitializer())
+ *
+ *
+ * @param synchronizers the synchronizer configurers, in priority order
+ * @return this builder
+ */
+ @SafeVarargs
+ public final ConnectionModeBuilder synchronizers(@NonNull ComponentConfigurer
+ * builder.synchronizers(
+ * DataSystemComponents.streamingSynchronizer(),
+ * DataSystemComponents.pollingSynchronizer())
+ *
+ *
+ * LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
+ * .mobileKey("my-key")
+ * .dataSystem(Components.dataSystem())
+ * .build();
+ *
+ *
+ * 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();
+ *
+ *
+ * Components.dataSystem()
+ * .foregroundConnectionMode(ConnectionMode.POLLING)
+ *
+ * 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.
+ *
+ * Components.dataSystem()
+ * .automaticModeSwitching(false)
+ * .foregroundConnectionMode(ConnectionMode.STREAMING)
+ *
+ *
+ * @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
+ * Components.dataSystem()
+ * .customizeConnectionMode(ConnectionMode.BACKGROUND,
+ * DataSystemComponents.customMode()
+ * .initializers(DataSystemComponents.pollingInitializer())
+ * .synchronizers(
+ * DataSystemComponents.pollingSynchronizer()
+ * .pollIntervalMillis(21_600_000)))
+ *
+ *
+ * DataSystemComponents.customMode()
+ * .initializers(DataSystemComponents.pollingInitializer())
+ *
+ *
+ * DataSystemComponents.customMode()
+ * .synchronizers(
+ * DataSystemComponents.pollingSynchronizer()
+ * .pollIntervalMillis(60_000))
+ *
+ *
+ * DataSystemComponents.customMode()
+ * .synchronizers(
+ * DataSystemComponents.streamingSynchronizer()
+ * .initialReconnectDelayMillis(500))
+ *