Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ce7fc5f
feat: Add FDv2 connection mode types and mode resolution table
aaron-zeisler Mar 9, 2026
4e28509
feat: Implement switchMode() on FDv2DataSource
aaron-zeisler Mar 10, 2026
9eb4f5b
feat: Add FDv2DataSourceBuilder with stub configurer resolution
aaron-zeisler Mar 10, 2026
9056026
feat: wire real FDv2 ComponentConfigurer implementations in FDv2DataS…
aaron-zeisler Mar 10, 2026
e0acb02
feat: Add FDv2 mode resolution to ConnectivityManager
aaron-zeisler Mar 10, 2026
38c0bc0
[SDK-1956] clean up unused code
aaron-zeisler Mar 12, 2026
c964cf9
[SDK-1956] refactor: switch to Approach 2 for FDv2 mode resolution an…
aaron-zeisler Mar 17, 2026
8873df6
[SDK-1956] refactor: ModeAware no longer extends DataSource
aaron-zeisler Mar 17, 2026
8754128
[SDK-1956] refactor: separate event processor and data source logic i…
aaron-zeisler Mar 17, 2026
71b3a96
[SDK-1956] refactor: move synchronizer switching into SourceManager t…
aaron-zeisler Mar 17, 2026
0ac33d1
[SDK-1956] refactor: move needsRefresh and FDv1/FDv2 branching into u…
aaron-zeisler Mar 18, 2026
62d9667
[SDK-1956] Replace internal switchMode() with teardown/rebuild at Con…
aaron-zeisler Mar 19, 2026
ca1c89e
[SDK-1956] Address PR review feedback
aaron-zeisler Mar 20, 2026
5b0a58a
[SDK-1956] Address Bugbot findings in FDv2 code
aaron-zeisler Mar 23, 2026
d423c6c
[SDK-1956] Address PR review comments and add FDv1 safety tests
aaron-zeisler Mar 24, 2026
9fad9ac
[SDK-1956] Remove redundant setActiveMode call from startUp
aaron-zeisler Mar 25, 2026
db5c1c9
chore: adds initial impl of data system configuration APIs (early acc…
tanderson-ld Mar 20, 2026
8c1ee24
chore: adds automatic switching config control
tanderson-ld Mar 24, 2026
42aa7c4
more tweaks
tanderson-ld Mar 26, 2026
2c959de
data system javadoc example tweaks
tanderson-ld Mar 26, 2026
8965cf2
adding unit tests
tanderson-ld Mar 27, 2026
1d3573a
refactor to use DataSourceBuilder instead of ComponentConfigurer
tanderson-ld Mar 27, 2026
661b707
adding shared executor to DataSourceBuilderInputs
tanderson-ld Mar 27, 2026
e03cc1f
merging main
tanderson-ld Mar 30, 2026
e894f37
Merge remote-tracking branch 'origin' into ta/SDK-1837/datasystem-con…
tanderson-ld Mar 30, 2026
6639048
fixing issue introduced during merge conflict resolution
tanderson-ld Mar 30, 2026
66cc8c1
adding e2e androidTests
tanderson-ld Apr 1, 2026
038d8a9
fixing minor bugbot issues
tanderson-ld Apr 1, 2026
a85839f
fixing partial change propagation and adding unit tests and e2e tests
tanderson-ld Apr 1, 2026
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
Expand Up @@ -4,10 +4,13 @@
import com.launchdarkly.sdk.LDContext;
import com.launchdarkly.sdk.android.env.IEnvironmentReporter;
import com.launchdarkly.sdk.android.subsystems.ClientContext;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.android.subsystems.DataSourceUpdateSink;
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
import com.launchdarkly.sdk.android.subsystems.TransactionalDataStore;
import com.launchdarkly.sdk.internal.events.DiagnosticStore;

import androidx.annotation.Nullable;

/**
* This package-private subclass of {@link ClientContext} contains additional non-public SDK objects
* that may be used by our internal components.
Expand All @@ -33,21 +36,42 @@ final class ClientContextImpl extends ClientContext {
private final PlatformState platformState;
private final TaskExecutor taskExecutor;
private final PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData;
@Nullable
private final TransactionalDataStore transactionalDataStore;

/** Used by FDv1 code paths that do not need a {@link TransactionalDataStore}. */
ClientContextImpl(
ClientContext base,
DiagnosticStore diagnosticStore,
FeatureFetcher fetcher,
PlatformState platformState,
TaskExecutor taskExecutor,
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData
) {
this(base, diagnosticStore, fetcher, platformState, taskExecutor, perEnvironmentData, null);
}

/**
* Used by FDv2 code paths. The {@code transactionalDataStore} is needed by
* {@link FDv2DataSourceBuilder} to create {@link SelectorSourceFacade} instances
* that provide selector state to initializers and synchronizers.
*/
ClientContextImpl(
ClientContext base,
DiagnosticStore diagnosticStore,
FeatureFetcher fetcher,
PlatformState platformState,
TaskExecutor taskExecutor,
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData,
@Nullable TransactionalDataStore transactionalDataStore
) {
super(base);
this.diagnosticStore = diagnosticStore;
this.fetcher = fetcher;
this.platformState = platformState;
this.taskExecutor = taskExecutor;
this.perEnvironmentData = perEnvironmentData;
this.transactionalDataStore = transactionalDataStore;
}

static ClientContextImpl fromConfig(
Expand Down Expand Up @@ -95,12 +119,30 @@ public static ClientContextImpl get(ClientContext context) {
return new ClientContextImpl(context, null, null, null, null, null);
}

/** Creates a context for FDv1 data sources that do not need a {@link TransactionalDataStore}. */
public static ClientContextImpl forDataSource(
ClientContext baseClientContext,
DataSourceUpdateSink dataSourceUpdateSink,
LDContext newEvaluationContext,
boolean newInBackground,
Boolean previouslyInBackground
) {
return forDataSource(baseClientContext, dataSourceUpdateSink, newEvaluationContext,
newInBackground, previouslyInBackground, null);
}

/**
* Creates a context for data sources, optionally including a {@link TransactionalDataStore}.
* FDv2 data sources require the store so that {@link FDv2DataSourceBuilder} can provide
* selector state to initializers and synchronizers via {@link SelectorSourceFacade}.
*/
public static ClientContextImpl forDataSource(
ClientContext baseClientContext,
DataSourceUpdateSink dataSourceUpdateSink,
LDContext newEvaluationContext,
boolean newInBackground,
Boolean previouslyInBackground,
@Nullable TransactionalDataStore transactionalDataStore
) {
ClientContextImpl baseContextImpl = ClientContextImpl.get(baseClientContext);
return new ClientContextImpl(
Expand All @@ -123,7 +165,8 @@ public static ClientContextImpl forDataSource(
baseContextImpl.getFetcher(),
baseContextImpl.getPlatformState(),
baseContextImpl.getTaskExecutor(),
baseContextImpl.getPerEnvironmentData()
baseContextImpl.getPerEnvironmentData(),
transactionalDataStore
);
}

Expand All @@ -139,7 +182,8 @@ public ClientContextImpl setEvaluationContext(LDContext context) {
this.fetcher,
this.platformState,
this.taskExecutor,
this.perEnvironmentData
this.perEnvironmentData,
this.transactionalDataStore
);
}

Expand All @@ -163,6 +207,11 @@ public PersistentDataStoreWrapper.PerEnvironmentData getPerEnvironmentData() {
return throwExceptionIfNull(perEnvironmentData);
}

@Nullable
public TransactionalDataStore getTransactionalDataStore() {
return transactionalDataStore;
}

private static <T> T throwExceptionIfNull(T o) {
if (o == null) {
throw new IllegalStateException(
Expand Down
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 @@ -101,14 +102,14 @@ public static ComponentConfigurer<EventProcessor> noEvents() {
* {@link PollingDataSourceBuilder} methods, and pass it to {@link LDConfig.Builder#dataSource(ComponentConfigurer)}:
* <pre><code>
* LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
* .dataSource(Components.pollingDataSource().initialReconnectDelayMillis(500))
* .dataSource(Components.pollingDataSource().pollIntervalMillis(900_000))
* .build();
* </code></pre>
* <p>
* Setting {@link LDConfig.Builder#offline(boolean)} to {@code true} will supersede this setting
* and completely disable network requests.
*
* @return a builder for setting streaming connection properties
* @return a builder for setting polling connection properties
* @see LDConfig.Builder#dataSource(ComponentConfigurer)
*/
public static PollingDataSourceBuilder pollingDataSource() {
Expand Down Expand Up @@ -221,4 +222,90 @@ 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>
* This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
* <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(AutomaticModeSwitchingConfig.disabled())
* .foregroundConnectionMode(ConnectionMode.STREAMING))
* .build();
* </code></pre>
* <p>
* <b>Example — disable lifecycle switching but keep network switching:</b>
* <pre><code>
* LDConfig config = new LDConfig.Builder(AutoEnvAttributes.Enabled)
* .mobileKey("my-key")
* .dataSystem(
* Components.dataSystem()
* .automaticModeSwitching(
* DataSystemComponents.automaticModeSwitching()
* .lifecycle(false)
* .network(true)
* .build()))
* .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
@@ -0,0 +1,76 @@
package com.launchdarkly.sdk.android;

/**
* Enumerates the built-in FDv2 connection modes. Each mode maps to a
* pipeline of initializers and synchronizers that are active when the SDK
* is operating in that mode.
* <p>
* This class is not stable, and not subject to any backwards compatibility guarantees or semantic versioning.
* It is in early access. If you want access to this feature please join the EAP. https://launchdarkly.com/docs/sdk/features/data-saving-mode
* <p>
* Not to be confused with {@link ConnectionInformation.ConnectionMode}, which
* is the public FDv1 enum representing the SDK's current connection state
* (e.g. POLLING, STREAMING, SET_OFFLINE). This class is an internal FDv2
* concept describing the <em>desired</em> data-acquisition pipeline.
* <p>
* This is a closed enum — custom connection modes (spec 5.3.5 TBD) are not
* supported in this release.
* <p>
* 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, when automatic mode switching is enabled, the SDK resolves a
* {@link ConnectionMode} from platform state in the same order as
* {@link ModeResolutionTable} (see {@link com.launchdarkly.sdk.android.integrations.DataSystemBuilder#foregroundConnectionMode}
* and {@link com.launchdarkly.sdk.android.integrations.DataSystemBuilder#backgroundConnectionMode}).
* The default built-in table ({@link ModeResolutionTable#MOBILE}) evaluates as follows:
* <ol>
* <li>No network &rarr; {@link #OFFLINE}</li>
* <li>Background with {@link LDConfig.Builder#disableBackgroundUpdating(boolean)} set
* &rarr; {@link #OFFLINE}</li>
* <li>Background (network available, background updates not disabled) &rarr;
* configured background mode (default {@link #BACKGROUND})</li>
* <li>Foreground with network &rarr; configured foreground mode (default {@link #STREAMING})</li>
* </ol>
*
* @see com.launchdarkly.sdk.android.integrations.DataSystemBuilder
* @see com.launchdarkly.sdk.android.integrations.ConnectionModeBuilder
*/
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");

/**
* 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;

private ConnectionMode(String name) {
this.name = name;
}

@Override
public String toString() {
return name;
}
}
Loading
Loading