Skip to content

Commit c91fb3b

Browse files
committed
[SDK-2001] feat: wire StateDebounceManager into ConnectivityManager for FDv2
Integrates the debounce manager into FDv2 data source lifecycle: - FDv2 network/lifecycle listeners route through debounce instead of immediate rebuild (CONNMODE 3.5.1-3.5.4) - Event processor state updated immediately in listeners (not debounced) - Events flushed before background transitions (CONNMODE 3.3.1) - identify() bypasses debounce by destroying/recreating the manager (CONNMODE 3.5.6) - setForceOffline() remains immediate (backward compatibility) - FDv1 code path is unchanged Adds integration tests for debounce coalescing, identify bypass, event flush timing, force-offline bypass, and shutdown cleanup. Made-with: Cursor
1 parent ad4d74c commit c91fb3b

2 files changed

Lines changed: 264 additions & 14 deletions

File tree

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

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class ConnectivityManager {
6666
private final TaskExecutor taskExecutor;
6767
private final boolean backgroundUpdatingDisabled;
6868
private final List<WeakReference<LDStatusListener>> statusListeners = new ArrayList<>();
69-
private final Debounce pollDebouncer = new Debounce();
69+
private final Debounce pollDebouncer = new Debounce(); // FDv1 only
7070
private final AtomicBoolean forcedOffline = new AtomicBoolean();
7171
private final AtomicBoolean started = new AtomicBoolean();
7272
private final AtomicBoolean closed = new AtomicBoolean();
@@ -79,6 +79,8 @@ class ConnectivityManager {
7979
private final ModeResolutionTable modeResolutionTable;
8080
private volatile ConnectionMode currentFDv2Mode;
8181
private final AutomaticModeSwitchingConfig autoModeSwitchingConfig;
82+
private long debounceMs = StateDebounceManager.DEFAULT_DEBOUNCE_MS;
83+
private volatile StateDebounceManager stateDebounceManager; // FDv2 only
8284

8385
// The DataSourceUpdateSinkImpl receives flag updates and status updates from the DataSource.
8486
// This has two purposes: 1. to decouple the data source implementation from the details of how
@@ -170,21 +172,49 @@ public void shutDown() {
170172
? ((FDv2DataSourceBuilder) dataSourceFactory).getResolutionTable()
171173
: null;
172174

175+
if (useFDv2ModeResolution) {
176+
this.stateDebounceManager = createDebounceManager();
177+
}
178+
173179
connectivityChangeListener = networkAvailable -> {
174-
if (useFDv2ModeResolution && !autoModeSwitchingConfig.isNetwork()) {
180+
if (useFDv2ModeResolution) {
181+
// Event processor state updated immediately so analytics events reflect reality.
175182
updateEventProcessor(forcedOffline.get(), platformState.isNetworkAvailable(), platformState.isForeground());
176-
return;
183+
if (!autoModeSwitchingConfig.isNetwork()) {
184+
return;
185+
}
186+
// CONNMODE 3.5.1: route through debounce window instead of immediate rebuild
187+
StateDebounceManager dm = stateDebounceManager;
188+
if (dm != null) {
189+
dm.setNetworkAvailable(networkAvailable);
190+
}
191+
} else {
192+
// FDv1 path: handleModeStateChange updates event processor internally
193+
handleModeStateChange();
177194
}
178-
handleModeStateChange();
179195
};
180196
platformState.addConnectivityChangeListener(connectivityChangeListener);
181197

182198
foregroundListener = foreground -> {
183-
if (useFDv2ModeResolution && !autoModeSwitchingConfig.isLifecycle()) {
199+
if (useFDv2ModeResolution) {
200+
// Event processor state updated immediately so analytics events reflect reality.
184201
updateEventProcessor(forcedOffline.get(), platformState.isNetworkAvailable(), platformState.isForeground());
185-
return;
202+
if (!autoModeSwitchingConfig.isLifecycle()) {
203+
return;
204+
}
205+
// CONNMODE 3.3.1: flush pending events before transitioning to background
206+
if (!foreground) {
207+
eventProcessor.flush();
208+
}
209+
// CONNMODE 3.5.1: route through debounce window
210+
StateDebounceManager dm = stateDebounceManager;
211+
if (dm != null) {
212+
dm.setForeground(foreground);
213+
}
214+
} else {
215+
// FDv1 path: handleModeStateChange updates event processor internally
216+
handleModeStateChange();
186217
}
187-
handleModeStateChange();
188218
};
189219
platformState.addForegroundChangeListener(foregroundListener);
190220
}
@@ -193,6 +223,10 @@ public void shutDown() {
193223
* Switches the {@link ConnectivityManager} to begin fetching/receiving information
194224
* relevant to the context provided. This is likely to result in the teardown of existing
195225
* connections, but the timing of that is not guaranteed.
226+
* <p>
227+
* CONNMODE 3.5.6: identify does NOT participate in debounce. The debounce manager is
228+
* destroyed and recreated so that any pending debounced state change is discarded and
229+
* the new context starts with a clean timer.
196230
*
197231
* @param context to swtich to
198232
* @param onCompletion callback that indicates when the switching is done
@@ -204,6 +238,15 @@ void switchToContext(@NonNull LDContext context, @NonNull Callback<Void> onCompl
204238
if (oldContext == context || oldContext.equals(context)) {
205239
onCompletion.onSuccess(null);
206240
} else {
241+
// CONNMODE 3.5.6: identify bypasses debounce — close and recreate the manager
242+
if (useFDv2ModeResolution) {
243+
StateDebounceManager oldDm = stateDebounceManager;
244+
if (oldDm != null) {
245+
oldDm.close();
246+
}
247+
stateDebounceManager = createDebounceManager();
248+
}
249+
207250
ModeState state = snapshotModeState();
208251
if (dataSource == null || dataSource.needsRefresh(!state.isForeground(), context)) {
209252
updateEventProcessor(forcedOffline.get(), state.isNetworkAvailable(), state.isForeground());
@@ -536,6 +579,11 @@ void shutDown() {
536579
if (closed.getAndSet(true)) {
537580
return;
538581
}
582+
StateDebounceManager dm = stateDebounceManager;
583+
if (dm != null) {
584+
dm.close();
585+
stateDebounceManager = null;
586+
}
539587
DataSource oldDataSource = currentDataSource.getAndSet(null);
540588
if (oldDataSource != null) {
541589
oldDataSource.stop(LDUtil.noOpCallback());
@@ -570,13 +618,64 @@ private void updateEventProcessor(boolean forceOffline, boolean networkAvailable
570618
* Unified handler for all platform/configuration state changes (foreground, connectivity,
571619
* force-offline). Snapshots the current state once, updates the event processor, then
572620
* routes to the appropriate data source update path.
621+
* <p>
622+
* FDv1 only — FDv2 state changes are routed through {@link StateDebounceManager} and
623+
* reconciled via {@link #handleDebouncedModeStateChange()}.
573624
*/
574625
private synchronized void handleModeStateChange() {
575626
ModeState state = snapshotModeState();
576627
updateEventProcessor(forcedOffline.get(), state.isNetworkAvailable(), state.isForeground());
577628
updateDataSource(false, state, LDUtil.noOpCallback());
578629
}
579630

631+
/**
632+
* Creates a new {@link StateDebounceManager} initialized with the current platform state.
633+
* Called once during construction (for FDv2) and again on each identify to discard pending
634+
* debounced changes (CONNMODE 3.5.6).
635+
*/
636+
/**
637+
* Sets the debounce window duration and recreates the debounce manager. Package-private
638+
* to allow tests to use a shorter debounce window for deterministic timing.
639+
*/
640+
void setDebounceMs(long ms) {
641+
this.debounceMs = ms;
642+
if (useFDv2ModeResolution) {
643+
StateDebounceManager oldDm = stateDebounceManager;
644+
if (oldDm != null) {
645+
oldDm.close();
646+
}
647+
stateDebounceManager = createDebounceManager();
648+
}
649+
}
650+
651+
private StateDebounceManager createDebounceManager() {
652+
return new StateDebounceManager(
653+
platformState.isNetworkAvailable(),
654+
platformState.isForeground(),
655+
taskExecutor,
656+
debounceMs,
657+
this::handleDebouncedModeStateChange
658+
);
659+
}
660+
661+
/**
662+
* Reconciliation callback invoked by the {@link StateDebounceManager} when the debounce
663+
* timer fires (CONNMODE 3.5.3). Reads the latest accumulated state from the debounce
664+
* manager and triggers a data source update if the resolved mode has changed.
665+
*/
666+
private void handleDebouncedModeStateChange() {
667+
StateDebounceManager dm = stateDebounceManager;
668+
if (dm == null) {
669+
return;
670+
}
671+
ModeState state = new ModeState(
672+
dm.isForeground(),
673+
dm.isNetworkAvailable(),
674+
backgroundUpdatingDisabled
675+
);
676+
updateDataSource(false, state, LDUtil.noOpCallback());
677+
}
678+
580679
private ModeState snapshotModeState() {
581680
return new ModeState(
582681
platformState.isForeground(),

0 commit comments

Comments
 (0)