Skip to content

Commit ee7bfc4

Browse files
committed
refactor: switch to Approach 2 for FDv2 mode resolution and switching
Replace Approach 1 implementation with Approach 2, which the team preferred for its cleaner architecture: - ConnectivityManager owns the resolved mode table and performs ModeState -> ConnectionMode -> ResolvedModeDefinition lookup - FDv2DataSource receives ResolvedModeDefinition via switchMode() and has no internal mode table - FDv2DataSourceBuilder uses a unified ComponentConfigurer-based code path for both production and test mode tables - ResolvedModeDefinition is a top-level class rather than an inner class of FDv2DataSource - ConnectionMode is a final class with static instances instead of a Java enum Made-with: Cursor
1 parent 070f859 commit ee7bfc4

16 files changed

Lines changed: 745 additions & 1038 deletions

docs/SDK-1956-development-plan.md

Lines changed: 66 additions & 56 deletions
Large diffs are not rendered by default.

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ClientContextImpl.java

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
package com.launchdarkly.sdk.android;
22

3-
import androidx.annotation.Nullable;
4-
53
import com.launchdarkly.logging.LDLogger;
64
import com.launchdarkly.sdk.LDContext;
75
import com.launchdarkly.sdk.android.env.IEnvironmentReporter;
86
import com.launchdarkly.sdk.android.subsystems.ClientContext;
9-
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
107
import com.launchdarkly.sdk.android.subsystems.DataSourceUpdateSink;
8+
import com.launchdarkly.sdk.android.subsystems.HttpConfiguration;
119
import com.launchdarkly.sdk.android.subsystems.TransactionalDataStore;
1210
import com.launchdarkly.sdk.internal.events.DiagnosticStore;
1311

12+
import androidx.annotation.Nullable;
13+
1414
/**
1515
* This package-private subclass of {@link ClientContext} contains additional non-public SDK objects
1616
* that may be used by our internal components.
@@ -36,6 +36,7 @@ final class ClientContextImpl extends ClientContext {
3636
private final PlatformState platformState;
3737
private final TaskExecutor taskExecutor;
3838
private final PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData;
39+
@Nullable
3940
private final TransactionalDataStore transactionalDataStore;
4041

4142
ClientContextImpl(
@@ -56,7 +57,7 @@ final class ClientContextImpl extends ClientContext {
5657
PlatformState platformState,
5758
TaskExecutor taskExecutor,
5859
PersistentDataStoreWrapper.PerEnvironmentData perEnvironmentData,
59-
TransactionalDataStore transactionalDataStore
60+
@Nullable TransactionalDataStore transactionalDataStore
6061
) {
6162
super(base);
6263
this.diagnosticStore = diagnosticStore;
@@ -119,22 +120,19 @@ public static ClientContextImpl forDataSource(
119120
boolean newInBackground,
120121
Boolean previouslyInBackground
121122
) {
122-
return forDataSource(baseClientContext, dataSourceUpdateSink, null,
123-
newEvaluationContext, newInBackground, previouslyInBackground);
123+
return forDataSource(baseClientContext, dataSourceUpdateSink, newEvaluationContext,
124+
newInBackground, previouslyInBackground, null);
124125
}
125126

126127
public static ClientContextImpl forDataSource(
127128
ClientContext baseClientContext,
128129
DataSourceUpdateSink dataSourceUpdateSink,
129-
@Nullable TransactionalDataStore transactionalDataStore,
130130
LDContext newEvaluationContext,
131131
boolean newInBackground,
132-
Boolean previouslyInBackground
132+
Boolean previouslyInBackground,
133+
@Nullable TransactionalDataStore transactionalDataStore
133134
) {
134135
ClientContextImpl baseContextImpl = ClientContextImpl.get(baseClientContext);
135-
TransactionalDataStore store = transactionalDataStore != null
136-
? transactionalDataStore
137-
: baseContextImpl.transactionalDataStore;
138136
return new ClientContextImpl(
139137
new ClientContext(
140138
baseClientContext.getMobileKey(),
@@ -156,7 +154,7 @@ public static ClientContextImpl forDataSource(
156154
baseContextImpl.getPlatformState(),
157155
baseContextImpl.getTaskExecutor(),
158156
baseContextImpl.getPerEnvironmentData(),
159-
store
157+
transactionalDataStore
160158
);
161159
}
162160

@@ -197,6 +195,7 @@ public PersistentDataStoreWrapper.PerEnvironmentData getPerEnvironmentData() {
197195
return throwExceptionIfNull(perEnvironmentData);
198196
}
199197

198+
@Nullable
200199
public TransactionalDataStore getTransactionalDataStore() {
201200
return transactionalDataStore;
202201
}
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,34 @@
11
package com.launchdarkly.sdk.android;
22

33
/**
4-
* Named connection modes for the FDv2 data system. Each mode maps to a
5-
* {@link ModeDefinition} that specifies which initializers and synchronizers to run.
4+
* Enumerates the built-in FDv2 connection modes. Each mode maps to a
5+
* {@link ModeDefinition} that specifies which initializers and synchronizers
6+
* are active when the SDK is operating in that mode.
7+
* <p>
8+
* This is a closed enum — custom connection modes (spec 5.3.5 TBD) are not
9+
* supported in this release.
610
* <p>
711
* Package-private — not part of the public SDK API.
812
*
913
* @see ModeDefinition
14+
* @see ModeResolutionTable
1015
*/
11-
enum ConnectionMode {
12-
STREAMING,
13-
POLLING,
14-
OFFLINE,
15-
ONE_SHOT,
16-
BACKGROUND
16+
final class ConnectionMode {
17+
18+
static final ConnectionMode STREAMING = new ConnectionMode("STREAMING");
19+
static final ConnectionMode POLLING = new ConnectionMode("POLLING");
20+
static final ConnectionMode OFFLINE = new ConnectionMode("OFFLINE");
21+
static final ConnectionMode ONE_SHOT = new ConnectionMode("ONE_SHOT");
22+
static final ConnectionMode BACKGROUND = new ConnectionMode("BACKGROUND");
23+
24+
private final String name;
25+
26+
private ConnectionMode(String name) {
27+
this.name = name;
28+
}
29+
30+
@Override
31+
public String toString() {
32+
return name;
33+
}
1734
}

launchdarkly-android-client-sdk/src/main/java/com/launchdarkly/sdk/android/ConnectivityManager.java

Lines changed: 45 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@ class ConnectivityManager {
5353
private final ClientContext baseClientContext;
5454
private final PlatformState platformState;
5555
private final ComponentConfigurer<DataSource> dataSourceFactory;
56-
private final TransactionalDataStore transactionalDataStore;
5756
private final DataSourceUpdateSink dataSourceUpdateSink;
5857
private final ConnectionInformationState connectionInformation;
5958
private final PersistentDataStoreWrapper.PerEnvironmentData environmentStore;
6059
private final EventProcessor eventProcessor;
60+
private final TransactionalDataStore transactionalDataStore;
6161
private final PlatformState.ForegroundChangeListener foregroundListener;
6262
private final PlatformState.ConnectivityChangeListener connectivityChangeListener;
6363
private final TaskExecutor taskExecutor;
@@ -72,6 +72,7 @@ class ConnectivityManager {
7272
private final AtomicReference<Boolean> previouslyInBackground = new AtomicReference<>();
7373
private final LDLogger logger;
7474
private volatile boolean initialized = false;
75+
private volatile Map<ConnectionMode, ResolvedModeDefinition> resolvedModeTable;
7576
private volatile ConnectionMode currentFDv2Mode;
7677

7778
// The DataSourceUpdateSinkImpl receives flag updates and status updates from the DataSource.
@@ -134,11 +135,11 @@ public void shutDown() {
134135
) {
135136
this.baseClientContext = clientContext;
136137
this.dataSourceFactory = dataSourceFactory;
137-
this.transactionalDataStore = contextDataManager;
138138
this.dataSourceUpdateSink = new DataSourceUpdateSinkImpl(contextDataManager);
139139
this.platformState = ClientContextImpl.get(clientContext).getPlatformState();
140140
this.eventProcessor = eventProcessor;
141141
this.environmentStore = environmentStore;
142+
this.transactionalDataStore = contextDataManager;
142143
this.taskExecutor = ClientContextImpl.get(clientContext).getTaskExecutor();
143144
this.logger = clientContext.getBaseLogger();
144145

@@ -153,7 +154,7 @@ public void shutDown() {
153154
connectivityChangeListener = networkAvailable -> {
154155
DataSource dataSource = currentDataSource.get();
155156
if (dataSource instanceof ModeAware) {
156-
eventProcessor.setOffline(forcedOffline.get() || !networkAvailable);
157+
eventProcessor.setOffline(!networkAvailable);
157158
resolveAndSwitchMode((ModeAware) dataSource);
158159
} else {
159160
updateDataSource(false, LDUtil.noOpCallback());
@@ -244,15 +245,21 @@ private synchronized boolean updateDataSource(
244245
ClientContext clientContext = ClientContextImpl.forDataSource(
245246
baseClientContext,
246247
dataSourceUpdateSink,
247-
transactionalDataStore,
248248
context,
249249
inBackground,
250-
previouslyInBackground.get()
250+
previouslyInBackground.get(),
251+
transactionalDataStore
251252
);
252253
DataSource dataSource = dataSourceFactory.build(clientContext);
253254
currentDataSource.set(dataSource);
254255
previouslyInBackground.set(Boolean.valueOf(inBackground));
255256

257+
if (dataSourceFactory instanceof FDv2DataSourceBuilder) {
258+
FDv2DataSourceBuilder fdv2Builder = (FDv2DataSourceBuilder) dataSourceFactory;
259+
resolvedModeTable = fdv2Builder.getResolvedModeTable();
260+
currentFDv2Mode = fdv2Builder.getStartingMode();
261+
}
262+
256263
dataSource.start(new Callback<Boolean>() {
257264
@Override
258265
public void onSuccess(Boolean result) {
@@ -272,10 +279,9 @@ public void onError(Throwable error) {
272279
}
273280
});
274281

275-
// Resolve the initial mode after start() so that switchMode() can safely replace
276-
// the source manager without conflicting with the start() task submission.
282+
// If the app starts in the background, the builder creates the data source with
283+
// STREAMING as the starting mode. Perform an initial mode resolution to correct this.
277284
if (dataSource instanceof ModeAware) {
278-
currentFDv2Mode = ConnectionMode.STREAMING;
279285
resolveAndSwitchMode((ModeAware) dataSource);
280286
}
281287

@@ -423,31 +429,6 @@ synchronized boolean startUp(@NonNull Callback<Void> onCompletion) {
423429
return updateDataSource(true, onCompletion);
424430
}
425431

426-
/**
427-
* Resolves the current platform state to a {@link ConnectionMode} using the mode resolution
428-
* table, and calls {@link ModeAware#switchMode} if the resolved mode differs from the current
429-
* mode. This replaces the legacy teardown/rebuild cycle for FDv2 data sources.
430-
*/
431-
private void resolveAndSwitchMode(ModeAware modeAware) {
432-
ConnectionMode resolvedMode;
433-
if (forcedOffline.get()) {
434-
resolvedMode = ConnectionMode.OFFLINE;
435-
} else {
436-
ModeState state = new ModeState(
437-
platformState.isForeground(),
438-
platformState.isNetworkAvailable()
439-
);
440-
resolvedMode = ModeResolutionTable.MOBILE.resolve(state);
441-
}
442-
443-
ConnectionMode previousMode = currentFDv2Mode;
444-
if (previousMode != resolvedMode) {
445-
logger.debug("Switching FDv2 data source mode: {} -> {}", previousMode, resolvedMode);
446-
currentFDv2Mode = resolvedMode;
447-
modeAware.switchMode(resolvedMode);
448-
}
449-
}
450-
451432
/**
452433
* Permanently stops data updating for the current client instance. We call this if the client
453434
* is being closed, or if we receive an error that indicates the mobile key is invalid.
@@ -481,6 +462,37 @@ boolean isForcedOffline() {
481462
return forcedOffline.get();
482463
}
483464

465+
/**
466+
* Resolves the current platform state to a ConnectionMode via the ModeResolutionTable,
467+
* looks up the ResolvedModeDefinition from the resolved mode table, and calls
468+
* switchMode() on the data source if the mode has changed.
469+
*/
470+
private void resolveAndSwitchMode(@NonNull ModeAware modeAware) {
471+
Map<ConnectionMode, ResolvedModeDefinition> table = resolvedModeTable;
472+
if (table == null) {
473+
return;
474+
}
475+
boolean forceOffline = forcedOffline.get();
476+
boolean networkAvailable = platformState.isNetworkAvailable();
477+
boolean foreground = platformState.isForeground();
478+
ModeState state = new ModeState(
479+
foreground && !forceOffline,
480+
networkAvailable && !forceOffline
481+
);
482+
ConnectionMode newMode = ModeResolutionTable.MOBILE.resolve(state);
483+
if (newMode == currentFDv2Mode) {
484+
return;
485+
}
486+
currentFDv2Mode = newMode;
487+
ResolvedModeDefinition def = table.get(newMode);
488+
if (def == null) {
489+
logger.warn("No resolved definition for mode {}; skipping switchMode", newMode);
490+
return;
491+
}
492+
logger.debug("Switching FDv2 mode to {}", newMode);
493+
modeAware.switchMode(def);
494+
}
495+
484496
synchronized ConnectionInformation getConnectionInformation() {
485497
return connectionInformation;
486498
}

0 commit comments

Comments
 (0)