diff --git a/packages/flutter_client_sdk/lib/launchdarkly_flutter_client_sdk.dart b/packages/flutter_client_sdk/lib/launchdarkly_flutter_client_sdk.dart index 820cad64..f896f38e 100644 --- a/packages/flutter_client_sdk/lib/launchdarkly_flutter_client_sdk.dart +++ b/packages/flutter_client_sdk/lib/launchdarkly_flutter_client_sdk.dart @@ -45,6 +45,24 @@ export 'package:launchdarkly_common_client/launchdarkly_common_client.dart' PersistenceConfig, ApplicationInfo, ConnectionMode, + FDv2ConnectionMode, + FDv2Streaming, + FDv2Polling, + FDv2Offline, + FDv2Background, + ResolvedConnectionMode, + ResolvedStreaming, + ResolvedPolling, + ResolvedBackground, + ResolvedOffline, + OfflineDetail, + OfflineSetOffline, + OfflineNetworkUnavailable, + OfflineBackgroundDisabled, + ModeState, + ModeResolutionEntry, + resolveMode, + flutterDefaultResolutionTable, Hook, HookMetadata, IdentifySeriesContext, diff --git a/packages/flutter_client_sdk/lib/src/config/defaults/flutter_default_config.dart b/packages/flutter_client_sdk/lib/src/config/defaults/flutter_default_config.dart index 615411a7..f727ac6f 100644 --- a/packages/flutter_client_sdk/lib/src/config/defaults/flutter_default_config.dart +++ b/packages/flutter_client_sdk/lib/src/config/defaults/flutter_default_config.dart @@ -1,20 +1,22 @@ +import 'package:launchdarkly_common_client/launchdarkly_common_client.dart'; + import 'stub_config.dart' if (dart.library.io) 'io_config.dart' if (dart.library.js_interop) 'js_config.dart'; /// Configuration common to web and mobile is contained in this file. /// -/// Configuration specific to either io targets or js targets are in io_config -/// and js_config and then exposed through this file. - -final class DefaultApplicationEventsConfig { - final defaultBackgrounding = true; - final defaultNetworkAvailability = true; -} +/// Native IO and web-specific defaults live in `io_config.dart` and +/// `js_config.dart` and are exposed through this file. final class FlutterDefaultConfig { static final ConnectionManagerConfig connectionManagerConfig = ConnectionManagerConfig(); - static final applicationEventsConfig = DefaultApplicationEventsConfig(); + /// Default automatic-resolution background slot. + static FDv2ConnectionMode get defaultBackgroundConnectionMode => + connectionManagerConfig.defaultBackgroundConnectionMode; + + static final ApplicationEventsConfig applicationEventsConfig = + ApplicationEventsConfig(); } diff --git a/packages/flutter_client_sdk/lib/src/config/defaults/io_config.dart b/packages/flutter_client_sdk/lib/src/config/defaults/io_config.dart index 63a8ddca..6e829672 100644 --- a/packages/flutter_client_sdk/lib/src/config/defaults/io_config.dart +++ b/packages/flutter_client_sdk/lib/src/config/defaults/io_config.dart @@ -1,6 +1,26 @@ import 'dart:io' show Platform; +import 'package:launchdarkly_common_client/launchdarkly_common_client.dart'; + class ConnectionManagerConfig { bool get runInBackground => Platform.isLinux || Platform.isWindows || Platform.isMacOS; + + FDv2ConnectionMode get defaultBackgroundConnectionMode => + Platform.isAndroid || Platform.isIOS || Platform.isFuchsia + ? const FDv2Background() + : const FDv2Offline(); +} + +/// Platform defaults for [ApplicationEvents] on native IO targets. +/// +/// Mobile uses application and network signals for automatic connection +/// handling; desktop IO targets do not by default. +final class ApplicationEventsConfig { + bool get _isMobile => + Platform.isAndroid || Platform.isIOS || Platform.isFuchsia; + + bool get defaultBackgrounding => _isMobile; + + bool get defaultNetworkAvailability => _isMobile; } diff --git a/packages/flutter_client_sdk/lib/src/config/defaults/js_config.dart b/packages/flutter_client_sdk/lib/src/config/defaults/js_config.dart index 23305631..fef88bb5 100644 --- a/packages/flutter_client_sdk/lib/src/config/defaults/js_config.dart +++ b/packages/flutter_client_sdk/lib/src/config/defaults/js_config.dart @@ -1,3 +1,17 @@ +import 'package:launchdarkly_common_client/launchdarkly_common_client.dart'; + class ConnectionManagerConfig { bool get runInBackground => true; + + FDv2ConnectionMode get defaultBackgroundConnectionMode => const FDv2Offline(); +} + +/// Platform defaults for [ApplicationEvents] on web. +/// +/// Web does not use application or network detector signals for automatic +/// connection handling by default. +final class ApplicationEventsConfig { + bool get defaultBackgrounding => false; + + bool get defaultNetworkAvailability => false; } diff --git a/packages/flutter_client_sdk/lib/src/config/defaults/stub_config.dart b/packages/flutter_client_sdk/lib/src/config/defaults/stub_config.dart index 2994e197..8d9fe3ba 100644 --- a/packages/flutter_client_sdk/lib/src/config/defaults/stub_config.dart +++ b/packages/flutter_client_sdk/lib/src/config/defaults/stub_config.dart @@ -1,3 +1,14 @@ +import 'package:launchdarkly_common_client/launchdarkly_common_client.dart'; + class ConnectionManagerConfig { bool get runInBackground => throw Exception('Stub implementation'); + + FDv2ConnectionMode get defaultBackgroundConnectionMode => const FDv2Offline(); +} + +/// Stub defaults for tests and unsupported compilation targets. +final class ApplicationEventsConfig { + bool get defaultBackgrounding => false; + + bool get defaultNetworkAvailability => false; } diff --git a/packages/flutter_client_sdk/lib/src/config/ld_config.dart b/packages/flutter_client_sdk/lib/src/config/ld_config.dart index 1c1cbbf7..d682c82b 100644 --- a/packages/flutter_client_sdk/lib/src/config/ld_config.dart +++ b/packages/flutter_client_sdk/lib/src/config/ld_config.dart @@ -16,13 +16,15 @@ final class ApplicationEvents { bool networkAvailability; /// Setting [backgrounding] to true allows the SDK to detect and react to - /// the application entering the background or foreground. The default - /// value is `true`. + /// the application entering the background or the foreground. The default + /// depends on the platform (typically enabled on mobile and disabled on + /// desktop and web); see [FlutterDefaultConfig.applicationEventsConfig]. /// /// Setting [networkAvailability] to true allows the SDK to detect and react /// to network connectivity changes. For instance the SDK may not try to send - /// events if it detects the network is not available. The default value is - /// `true`. + /// events if it detects the network is not available. The default depends on + /// the platform (typically enabled on mobile and disabled on desktop and web); + /// see [FlutterDefaultConfig.applicationEventsConfig]. ApplicationEvents({bool? backgrounding, bool? networkAvailability}) : backgrounding = backgrounding ?? FlutterDefaultConfig.applicationEventsConfig.defaultBackgrounding, diff --git a/packages/flutter_client_sdk/lib/src/connection_manager.dart b/packages/flutter_client_sdk/lib/src/connection_manager.dart index 010c1a69..142fa48f 100644 --- a/packages/flutter_client_sdk/lib/src/connection_manager.dart +++ b/packages/flutter_client_sdk/lib/src/connection_manager.dart @@ -35,7 +35,7 @@ abstract interface class StateDetector { /// be tested. The LDCommonClient doesn't implement this, so there is a small /// private adapter. abstract interface class ConnectionDestination { - void setMode(ConnectionMode mode); + void setMode(ResolvedConnectionMode mode); void setNetworkAvailability(bool available); @@ -51,8 +51,8 @@ final class DartClientAdapter implements ConnectionDestination { DartClientAdapter(this._client); @override - void setMode(ConnectionMode mode) { - _client.setMode(mode); + void setMode(ResolvedConnectionMode mode) { + _client.setResolvedMode(mode); } @override @@ -72,9 +72,16 @@ final class DartClientAdapter implements ConnectionDestination { } final class ConnectionManagerConfig { - /// The initial connection mode the SDK should use. + /// Configured foreground connection mode used as the automatic resolution + /// foreground slot. final ConnectionMode initialConnectionMode; + /// Configured background connection mode used as the automatic resolution + /// background slot. + /// + /// Defaults to [const FDv2Offline()]. + final FDv2ConnectionMode backgroundConnectionMode; + /// Some platforms (windows, web, mac, linux) can continue executing code /// in the background. final bool runInBackground; @@ -88,24 +95,28 @@ final class ConnectionManagerConfig { final bool disableAutomaticNetworkHandling; /// Disable handling associated with transitioning between the foreground - /// and background. This means that an application will make no attempt to - /// disconnect when entering background state, and it will not attempt - /// to re-establish a connection entering the foreground, beyond the standard + /// and background. This means that an application will not automatically + /// disconnect when entering background state, and it will not automatically + /// re-establish a connection entering the foreground, beyond the standard /// retry logic. - /// - /// The application will always be treated as in the foreground. final bool disableAutomaticBackgroundHandling; - ConnectionManagerConfig( - {this.initialConnectionMode = ConnectionMode.streaming, - this.runInBackground = true, - this.disableAutomaticBackgroundHandling = false, - this.disableAutomaticNetworkHandling = false}); + ConnectionManagerConfig({ + this.initialConnectionMode = ConnectionMode.streaming, + this.backgroundConnectionMode = const FDv2Offline(), + this.runInBackground = true, + this.disableAutomaticBackgroundHandling = false, + this.disableAutomaticNetworkHandling = false, + }); } /// This class tracks the state of the application, network, configuration, /// and desired network state. It uses this information to request specific -/// data source configurations. +/// connection modes. +/// +/// Automatic resolution uses [resolveMode] with +/// [flutterDefaultResolutionTable] by default, or [resolutionTable] when +/// supplied to the constructor. /// /// This class does not contain any platform specific code. Instead platform /// specific code should be implemented in a [StateDetector]. This is primarily @@ -115,11 +126,15 @@ final class ConnectionManager { final ConnectionManagerConfig _config; final StateDetector _detector; final ConnectionDestination _destination; + final List _resolutionTable; StreamSubscription? _applicationStateSub; StreamSubscription? _networkStateSub; - ConnectionMode _currentConnectionMode; + /// When non-null, [resolveMode] is skipped and this mode is + /// applied regardless of lifecycle/network. + FDv2ConnectionMode? _modeOverride; + ApplicationState _applicationState; NetworkState _networkState; @@ -132,21 +147,24 @@ final class ConnectionManager { _handleState(); } - ConnectionManager( - {required LDLogger logger, - required ConnectionManagerConfig config, - required ConnectionDestination destination, - required StateDetector detector}) - : _logger = logger.subLogger('ConnectionManager'), + ConnectionManager({ + required LDLogger logger, + required ConnectionManagerConfig config, + required ConnectionDestination destination, + required StateDetector detector, + List? resolutionTable, + }) : _logger = logger.subLogger('ConnectionManager'), _config = config, _destination = destination, - _currentConnectionMode = config.initialConnectionMode, + _resolutionTable = resolutionTable ?? flutterDefaultResolutionTable(), _applicationState = ApplicationState.foreground, _networkState = NetworkState.available, _detector = detector { if (!_config.disableAutomaticBackgroundHandling) { _applicationStateSub = detector.applicationState.listen((applicationState) { + // TODO (SDK-2187): plumb in debouncer here + _applicationState = applicationState; _handleState(); }); @@ -154,59 +172,53 @@ final class ConnectionManager { if (!_config.disableAutomaticNetworkHandling) { _networkStateSub = detector.networkState.listen((networkState) { + // TODO (SDK-2187): plumb in debouncer here + _networkState = networkState; + _destination + .setNetworkAvailability(networkState == NetworkState.available); _handleState(); }); } } - void _setForegroundAvailableMode() { - if (offline) { - _destination.setMode(ConnectionMode.offline); - _destination.setEventSendingEnabled(false, flush: false); - return; - } - - // Currently the foreground mode will always be whatever the last active - // connection mode was. - _destination.setMode(_currentConnectionMode); - _destination.setEventSendingEnabled(true); - } - - void _setBackgroundAvailableMode() { - // flush on backgrounding as application may be killed and we don't want to lose events. - _destination.flush(); - - if (!_config.runInBackground) { - // TODO: Can we support the backgroundDisabled data source status? - // TODO: Is it acceptable for the data source status and `offline` to - // report an `offline` status? - _destination.setMode(ConnectionMode.offline); + void _handleState() { + _logger.debug('Handling state: $_applicationState:$_networkState'); - // no need to flush here, we just did up above - _destination.setEventSendingEnabled(false, flush: false); - return; + final networkAvailable = _networkState == NetworkState.available; + final inForeground = _applicationState == ApplicationState.foreground; + + final ResolvedConnectionMode resolved; + if (_offline) { + resolved = const ResolvedOffline(OfflineSetOffline()); + } else if (_modeOverride case final mode?) { + resolved = switch (mode) { + FDv2Streaming() => const ResolvedStreaming(), + FDv2Polling() => const ResolvedPolling(), + FDv2Background() => const ResolvedBackground(), + FDv2Offline() => const ResolvedOffline(OfflineSetOffline()), + }; + } else { + final modeState = ModeState( + networkAvailable: networkAvailable, + inForeground: inForeground, + runInBackground: _config.runInBackground, + foregroundConnectionMode: _fdv2FromFdv1(_config.initialConnectionMode), + backgroundConnectionMode: _config.backgroundConnectionMode, + ); + resolved = resolveMode(_resolutionTable, modeState); } - // If connections in the background are allowed, then use the same mode - // as is configured for the foreground. - _setForegroundAvailableMode(); - } + if (!_offline && !inForeground && networkAvailable) { + _destination.flush(); + } - void _handleState() { - _logger.debug('Handling state: $_applicationState:$_networkState'); + _destination.setMode(resolved); - switch (_networkState) { - case NetworkState.unavailable: - _destination.setNetworkAvailability(false); - case NetworkState.available: - _destination.setNetworkAvailability(true); - switch (_applicationState) { - case ApplicationState.foreground: - _setForegroundAvailableMode(); - case ApplicationState.background: - _setBackgroundAvailableMode(); - } + if (_offline || (!inForeground && !_config.runInBackground)) { + _destination.setEventSendingEnabled(false, flush: false); + } else { + _destination.setEventSendingEnabled(true); } } @@ -219,9 +231,16 @@ final class ConnectionManager { _detector.dispose(); } - /// Set the desired connection mode for the SDK. - void setMode(ConnectionMode mode) { - _currentConnectionMode = mode; + /// Set the desired connection mode for the SDK. Passing null clears the + /// override and resumes automatic mode resolution. + void setMode(FDv2ConnectionMode? mode) { + _modeOverride = mode; _handleState(); } } + +FDv2ConnectionMode _fdv2FromFdv1(ConnectionMode mode) => switch (mode) { + ConnectionMode.streaming => const FDv2Streaming(), + ConnectionMode.polling => const FDv2Polling(), + ConnectionMode.offline => const FDv2Offline(), + }; diff --git a/packages/flutter_client_sdk/lib/src/ld_client.dart b/packages/flutter_client_sdk/lib/src/ld_client.dart index ecaa3f47..3419305d 100644 --- a/packages/flutter_client_sdk/lib/src/ld_client.dart +++ b/packages/flutter_client_sdk/lib/src/ld_client.dart @@ -78,9 +78,10 @@ interface class LDClient { _connectionManager = ConnectionManager( logger: _client.logger, config: ConnectionManagerConfig( - initialConnectionMode: config.offline - ? ConnectionMode.offline - : config.dataSourceConfig.initialConnectionMode, + initialConnectionMode: + config.dataSourceConfig.initialConnectionMode, + backgroundConnectionMode: + FlutterDefaultConfig.defaultBackgroundConnectionMode, disableAutomaticBackgroundHandling: config.offline || !config.applicationEvents.backgrounding, disableAutomaticNetworkHandling: @@ -90,6 +91,10 @@ interface class LDClient { destination: DartClientAdapter(_client), detector: FlutterStateDetector()); + if (config.offline) { + _connectionManager.offline = true; + } + final sdkPluginMetadata = PluginSdkMetadata(name: sdkName, version: sdkVersion); diff --git a/packages/flutter_client_sdk/test/persistence/connection_manager_test.dart b/packages/flutter_client_sdk/test/persistence/connection_manager_test.dart index 862a0937..2b77d3b4 100644 --- a/packages/flutter_client_sdk/test/persistence/connection_manager_test.dart +++ b/packages/flutter_client_sdk/test/persistence/connection_manager_test.dart @@ -57,6 +57,9 @@ void main() { message: '', time: DateTime.now(), logTag: '')); + registerFallbackValue(const OfflineSetOffline()); + registerFallbackValue(const ResolvedStreaming()); + registerFallbackValue(const ResolvedOffline(OfflineSetOffline())); }); test('it can set the connection offline when entering the background', @@ -80,14 +83,15 @@ void main() { // Wait for the state to propagate. await mockDetector.applicationState.first; - verify(() => destination.setMode(ConnectionMode.offline)); + verify(() => destination + .setMode(const ResolvedOffline(OfflineBackgroundDisabled()))); connectionManager.dispose(); }); group('given default connection modes', () { for (var initialMode in [ ConnectionMode.streaming, - ConnectionMode.polling + ConnectionMode.polling, ]) { test( 'it can restore the connection when entering the foreground for mode: $initialMode', @@ -112,7 +116,8 @@ void main() { // Wait for the state to propagate. await mockDetector.applicationState.first; - verify(() => destination.setMode(ConnectionMode.offline)); + verify(() => destination + .setMode(const ResolvedOffline(OfflineBackgroundDisabled()))); reset(destination); mockDetector.setApplicationState(ApplicationState.foreground); @@ -120,14 +125,20 @@ void main() { // Wait for the state to propagate. await mockDetector.applicationState.first; - verify(() => destination.setMode(initialMode)); + verify(() => destination.setMode(switch (initialMode) { + ConnectionMode.streaming => const ResolvedStreaming(), + ConnectionMode.polling => const ResolvedPolling(), + ConnectionMode.offline => + const ResolvedOffline(OfflineSetOffline()), + })); connectionManager.dispose(); }); } }); test( - 'if runInBackground is true, then it remains online when entering the background', + 'if runInBackground is true, default background slot is offline ' + '(desktop-style automatic resolution / default ConnectionManagerConfig)', () async { registerFallbackValue(ConnectionMode.streaming); @@ -149,7 +160,67 @@ void main() { await mockDetector.applicationState.first; verify(() => destination.flush()); - verify(() => destination.setMode(ConnectionMode.streaming)); + verify( + () => destination.setMode(const ResolvedOffline(OfflineSetOffline()))); + connectionManager.dispose(); + }); + + test( + 'if runInBackground is true and backgroundConnectionMode is background, ' + 'it uses that slot in the background', () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(const FDv2Background()); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + runInBackground: true, + backgroundConnectionMode: const FDv2Background(), + ); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector); + + mockDetector.setApplicationState(ApplicationState.background); + + await mockDetector.applicationState.first; + + verify(() => destination.flush()); + verify(() => destination.setMode(const ResolvedBackground())); + connectionManager.dispose(); + }); + + test( + 'if runInBackground is true and backgroundConnectionMode is streaming, ' + 'it uses that slot in the background', () async { + registerFallbackValue(ConnectionMode.streaming); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + runInBackground: true, + backgroundConnectionMode: const FDv2Streaming(), + ); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector); + + mockDetector.setApplicationState(ApplicationState.background); + + await mockDetector.applicationState.first; + + verify(() => destination.flush()); + verify(() => destination.setMode(const ResolvedStreaming())); connectionManager.dispose(); }); @@ -176,6 +247,8 @@ void main() { await mockDetector.networkState.first; verify(() => destination.setNetworkAvailability(false)); + verify(() => destination + .setMode(const ResolvedOffline(OfflineNetworkUnavailable()))); connectionManager.dispose(); }); @@ -213,6 +286,156 @@ void main() { connectionManager.dispose(); }); + group('network drives mode resolution and custom resolution tables', () { + test( + 'when network is unavailable in the background, mode is offline ' + 'not the background slot (first table row wins)', () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(const FDv2Background()); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + runInBackground: true, + backgroundConnectionMode: const FDv2Background(), + ); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector, + ); + + mockDetector.setApplicationState(ApplicationState.background); + await mockDetector.applicationState.first; + + verify(() => destination.setMode(const ResolvedBackground())); + reset(destination); + + mockDetector.setNetworkAvailable(false); + await mockDetector.networkState.first; + + verify(() => destination.setNetworkAvailability(false)); + verify(() => destination + .setMode(const ResolvedOffline(OfflineNetworkUnavailable()))); + connectionManager.dispose(); + }); + + test( + 'when network returns while foreground, restores ' + 'initialConnectionMode from automatic resolution', () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(ConnectionMode.polling); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + initialConnectionMode: ConnectionMode.polling, + runInBackground: true, + ); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector, + ); + + mockDetector.setNetworkAvailable(false); + await mockDetector.networkState.first; + + verify(() => destination + .setMode(const ResolvedOffline(OfflineNetworkUnavailable()))); + reset(destination); + + mockDetector.setNetworkAvailable(true); + await mockDetector.networkState.first; + + verify(() => destination.setNetworkAvailability(true)); + verify(() => destination.setMode(const ResolvedPolling())); + connectionManager.dispose(); + }); + + test( + 'custom resolution table: network row only then fallback to ' + 'initialConnectionMode when network is available', () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(ConnectionMode.polling); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + initialConnectionMode: ConnectionMode.polling, + runInBackground: true, + ); + final mockDetector = MockStateDetector(); + + final customTable = [ + ModeResolutionEntry( + predicate: (ModeState s) => !s.networkAvailable, + resolve: (_) => const ResolvedOffline(OfflineNetworkUnavailable()), + ), + ]; + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector, + resolutionTable: customTable, + ); + + mockDetector.setNetworkAvailable(false); + await mockDetector.networkState.first; + + verify(() => destination + .setMode(const ResolvedOffline(OfflineNetworkUnavailable()))); + reset(destination); + + mockDetector.setNetworkAvailable(true); + await mockDetector.networkState.first; + + verify(() => destination.setMode(const ResolvedPolling())); + connectionManager.dispose(); + }); + + test( + 'custom empty resolution table falls back to initialConnectionMode ' + 'for all automatic states', () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(ConnectionMode.polling); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig( + initialConnectionMode: ConnectionMode.polling, + runInBackground: true, + ); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector, + resolutionTable: const [], + ); + + mockDetector.setNetworkAvailable(false); + await mockDetector.networkState.first; + + verify(() => destination.setMode(const ResolvedPolling())); + connectionManager.dispose(); + }); + }); + test('when temporarily offline it ignores state changes and remains offline', () async { registerFallbackValue(ConnectionMode.streaming); @@ -231,7 +454,8 @@ void main() { connectionManager.offline = true; - verify(() => destination.setMode(ConnectionMode.offline)); + verify( + () => destination.setMode(const ResolvedOffline(OfflineSetOffline()))); verify(() => destination.setEventSendingEnabled(false, flush: false)); reset(destination); @@ -242,7 +466,8 @@ void main() { await mockDetector.applicationState.first; await mockDetector.networkState.first; - verify(() => destination.setMode(ConnectionMode.offline)); + verify( + () => destination.setMode(const ResolvedOffline(OfflineSetOffline()))); verify(() => destination.setNetworkAvailability(true)); verify(() => destination.setEventSendingEnabled(false, flush: false)); connectionManager.dispose(); @@ -306,24 +531,55 @@ void main() { connectionManager.dispose(); }); + test('setMode override: applies in background, null restores automatic table', + () async { + registerFallbackValue(ConnectionMode.streaming); + registerFallbackValue(ConnectionMode.polling); + + final destination = MockDestination(); + final logAdapter = MockLogAdapter(); + final logger = LDLogger(adapter: logAdapter); + final config = ConnectionManagerConfig(runInBackground: true); + final mockDetector = MockStateDetector(); + + final connectionManager = ConnectionManager( + logger: logger, + config: config, + destination: destination, + detector: mockDetector); + + mockDetector.setApplicationState(ApplicationState.background); + await mockDetector.applicationState.first; + + verify( + () => destination.setMode(const ResolvedOffline(OfflineSetOffline()))); + reset(destination); + + connectionManager.setMode(const FDv2Polling()); + verify(() => destination.setMode(const ResolvedPolling())); + reset(destination); + + connectionManager.setMode(null); + verify( + () => destination.setMode(const ResolvedOffline(OfflineSetOffline()))); + connectionManager.dispose(); + }); + group('given requested connection modes', () { - for (var requestedMode in [ - ConnectionMode.streaming, - ConnectionMode.polling, - ConnectionMode.offline, + for (var entry in <(FDv2ConnectionMode, ResolvedConnectionMode)>[ + (const FDv2Streaming(), const ResolvedStreaming()), + (const FDv2Polling(), const ResolvedPolling()), + (const FDv2Background(), const ResolvedBackground()), + (const FDv2Offline(), const ResolvedOffline(OfflineSetOffline())), ]) { - test('it respects changes to the desired connection mode', () { - // Get an initial mode that will be different than the requested mode. - final initialMode = - ConnectionMode.values.firstWhere((mode) => mode != requestedMode); - + final (requestedMode, expectedResolved) = entry; + test('it respects setMode($requestedMode)', () { registerFallbackValue(ConnectionMode.streaming); final destination = MockDestination(); final logAdapter = MockLogAdapter(); final logger = LDLogger(adapter: logAdapter); - final config = ConnectionManagerConfig( - runInBackground: false, initialConnectionMode: initialMode); + final config = ConnectionManagerConfig(runInBackground: false); final mockDetector = MockStateDetector(); final connectionManager = ConnectionManager( @@ -335,7 +591,7 @@ void main() { reset(destination); connectionManager.setMode(requestedMode); - verify(() => destination.setMode(requestedMode)); + verify(() => destination.setMode(expectedResolved)); verifyNever( () => destination.setEventSendingEnabled(true, flush: false)); connectionManager.dispose();