Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -221,4 +222,73 @@ public static HooksConfigurationBuilder hooks() {
public static PluginsConfigurationBuilder plugins() {
return new ComponentsImpl.PluginsConfigurationBuilderImpl();
}

/**
* Returns a builder for configuring the data system.
* <p>
* 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.
* <p>
* 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.
* <p>
* <b>Example — opting in to use the default data system:</b>
* <pre><code>
* LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
* .mobileKey("my-key")
* .dataSystem(Components.dataSystem())
* .build();
* </code></pre>
* <p>
* <b>Example — customize background polling to once every 6 hours:</b>
* <pre><code>
* 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();
* </code></pre>
* <p>
* <b>Example — use polling instead of streaming in the foreground:</b>
* <pre><code>
* LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
* .mobileKey("my-key")
* .dataSystem(
* Components.dataSystem()
* .foregroundConnectionMode(ConnectionMode.POLLING))
* .build();
* </code></pre>
* <p>
* <b>Example — disable automatic mode switching:</b>
* <pre><code>
* LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
* .mobileKey("my-key")
* .dataSystem(
* Components.dataSystem()
* .automaticModeSwitching(false)
* .foregroundConnectionMode(ConnectionMode.STREAMING))
* .build();
* </code></pre>
* <p>
* 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* Not to be confused with {@link ConnectionInformation.ConnectionMode}, which
* is the public FDv1 enum representing the SDK's current connection state
Expand All @@ -13,18 +13,47 @@
* This is a closed enum — custom connection modes (spec 5.3.5 TBD) are not
* supported in this release.
* <p>
* 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.
* <p>
* On mobile, the SDK automatically transitions between modes based on
* platform state (foreground/background, network availability). The default
* resolution is:
* <ul>
* <li>No network &rarr; {@link #OFFLINE}</li>
* <li>Background &rarr; {@link #BACKGROUND}</li>
* <li>Foreground &rarr; {@link #STREAMING}</li>
* </ul>
*
* @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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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}.
* <p>
* 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}.
* <p>
* <b>Example:</b>
* <pre><code>
* 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();
* </code></pre>
*
* @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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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)}.
* <pre><code>
* 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();
* </code></pre>
*
* @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;
}
}
Loading
Loading