Skip to content
Draft
2 changes: 1 addition & 1 deletion open_wearable/docs/app-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Two layers are used:

- `WearableConnector` (`lib/models/wearable_connector.dart`)
- Direct connection API and event stream for connect/disconnect events.
- `BluetoothAutoConnector` (`lib/models/bluetooth_auto_connector.dart`)
- `BluetoothAutoConnector` (`lib/models/auto_connector/bluetooth_auto_connector.dart`)
- Reconnect workflow based on remembered device names and user preference.

`MyApp` subscribes to connector/provider event streams to:
Expand Down
33 changes: 29 additions & 4 deletions open_wearable/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:open_wearable/models/app_shutdown_settings.dart';
import 'package:open_wearable/models/app_upgrade_coordinator.dart';
import 'package:open_wearable/models/app_upgrade_highlight.dart';
import 'package:open_wearable/models/auto_connect_preferences.dart';
import 'package:open_wearable/models/connect_devices_scan_session.dart';
import 'package:open_wearable/models/log_file_manager.dart';
import 'package:open_wearable/models/fota_post_update_verification.dart';
import 'package:open_wearable/models/wearable_connector.dart'
Expand All @@ -26,8 +27,10 @@ import 'package:open_wearable/widgets/updates/app_upgrade_page.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'models/bluetooth_auto_connector.dart';
import 'models/auto_connector/bluetooth_auto_connector.dart';
import 'models/auto_connector/system_auto_connector.dart';
import 'models/logger.dart';
import 'models/permissions_handler.dart';
import 'view_models/app_banner_controller.dart';
import 'view_models/wearables_provider.dart';

Expand Down Expand Up @@ -56,10 +59,19 @@ void main() async {
return provider;
},
),
Provider.value(value: WearableConnector()),
ChangeNotifierProvider(
create: (context) => AppBannerController(),
),
Provider<PermissionsHandler>(
create: (context) => PermissionsHandler(
navigatorGetter: () => rootNavigatorKey.currentState,
),
),
Provider<WearableConnector>(
create: (context) => WearableConnector(
permissionsHandler: context.read<PermissionsHandler>(),
),
),
ChangeNotifierProvider.value(value: logFileManager),
],
child: const MyApp(),
Expand All @@ -80,6 +92,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
late final StreamSubscription _wearableEventSub;
late final StreamSubscription<AvailabilityState> _bleAvailabilitySub;
late final BluetoothAutoConnector _autoConnector;
late final SystemAutoConnector _systemAutoConnector;
late final WearableConnector _wearableConnector;
late final Future<SharedPreferences> _prefsFuture;
late final StreamSubscription _wearableProvEventSub;
Expand Down Expand Up @@ -232,12 +245,21 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
});

_wearableConnector = context.read<WearableConnector>();
final permissionsHandler = context.read<PermissionsHandler>();
ConnectDevicesScanSession.configure(
permissionsHandler: permissionsHandler,
);

_autoConnector = BluetoothAutoConnector(
navStateGetter: () => rootNavigatorKey.currentState,
connector: _wearableConnector,
wearableManager: WearableManager(),
permissionsHandler: permissionsHandler,
prefsFuture: _prefsFuture,
onWearableConnected: _handleWearableConnected,
);
_systemAutoConnector = SystemAutoConnector(
connector: _wearableConnector,
wearableManager: WearableManager(),
permissionsHandler: permissionsHandler,
);
AutoConnectPreferences.autoConnectEnabledListenable.addListener(
_syncAutoConnectorWithSetting,
Expand Down Expand Up @@ -298,9 +320,11 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
void _syncAutoConnectorWithSetting() {
if (AutoConnectPreferences.autoConnectEnabled && _isBluetoothPoweredOn) {
_autoConnector.start();
_systemAutoConnector.start();
return;
}
_autoConnector.stop();
_systemAutoConnector.stop();
}

Future<void> _syncInitialBluetoothAvailability() async {
Expand Down Expand Up @@ -736,6 +760,7 @@ class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
_setBackgroundExecutionForShutdown(false);
_setBackgroundExecutionForRecording(false);
_autoConnector.stop();
_systemAutoConnector.stop();
super.dispose();
}

Expand Down
78 changes: 78 additions & 0 deletions open_wearable/lib/models/auto_connect_preferences.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,43 @@ class AutoConnectPreferences {
StreamController<void>.broadcast();
static final ValueNotifier<bool> _autoConnectEnabledNotifier =
ValueNotifier<bool>(true);
static final ValueNotifier<List<String>> _rememberedDeviceNamesNotifier =
ValueNotifier<List<String>>(const <String>[]);

/// Broadcasts any persisted auto-connect preference change.
static Stream<void> get changes => _changesController.stream;

/// Exposes the stored Bluetooth auto-connect toggle state.
static ValueListenable<bool> get autoConnectEnabledListenable =>
_autoConnectEnabledNotifier;

/// Returns the currently cached Bluetooth auto-connect toggle state.
static bool get autoConnectEnabled => _autoConnectEnabledNotifier.value;

/// Exposes the normalized remembered device names used for auto-connect.
static ValueListenable<List<String>> get rememberedDeviceNamesListenable =>
_rememberedDeviceNamesNotifier;

/// Returns the currently cached remembered device names used for
/// auto-connect.
static List<String> get rememberedDeviceNames =>
List<String>.unmodifiable(_rememberedDeviceNamesNotifier.value);

/// Loads the persisted auto-connect settings into the in-memory notifiers.
static Future<void> initialize() async {
await loadAutoConnectEnabled();
await loadRememberedDeviceNames();
}

/// Loads the persisted auto-connect enabled flag from storage.
static Future<bool> loadAutoConnectEnabled() async {
final prefs = await SharedPreferences.getInstance();
final enabled = prefs.getBool(autoConnectEnabledKey) ?? true;
_setAutoConnectEnabled(enabled);
return enabled;
}

/// Persists the auto-connect enabled flag and updates listeners on success.
static Future<bool> saveAutoConnectEnabled(bool enabled) async {
final prefs = await SharedPreferences.getInstance();
final success = await prefs.setBool(autoConnectEnabledKey, enabled);
Expand All @@ -49,6 +69,15 @@ class AutoConnectPreferences {
return enabled;
}

/// Loads the remembered auto-connect device names from storage.
static Future<List<String>> loadRememberedDeviceNames() async {
final prefs = await SharedPreferences.getInstance();
final rememberedNames = readRememberedDeviceNames(prefs);
_setRememberedDeviceNames(rememberedNames);
return rememberedNames;
}

/// Reads normalized remembered device names from the provided preferences.
static List<String> readRememberedDeviceNames(SharedPreferences prefs) {
final names =
prefs.getStringList(connectedDeviceNamesKey) ?? const <String>[];
Expand All @@ -65,6 +94,7 @@ class AutoConnectPreferences {
return normalizedNames;
}

/// Counts how often a device name appears in the remembered device list.
static int countRememberedDeviceName(
SharedPreferences prefs,
String deviceName,
Expand All @@ -77,6 +107,7 @@ class AutoConnectPreferences {
return names.where((name) => name == normalizedName).length;
}

/// Persists a remembered device name for future background auto-connect.
static Future<void> rememberDeviceName(
SharedPreferences prefs,
String deviceName,
Expand All @@ -93,10 +124,15 @@ class AutoConnectPreferences {
normalizedName,
]);
if (success) {
_setRememberedDeviceNames(<String>[
...names,
normalizedName,
]);
_changesController.add(null);
}
}

/// Removes one remembered device-name entry from the auto-connect targets.
static Future<void> forgetDeviceName(
SharedPreferences prefs,
String deviceName,
Expand All @@ -118,6 +154,38 @@ class AutoConnectPreferences {
updatedNames,
);
if (success) {
_setRememberedDeviceNames(updatedNames);
_changesController.add(null);
}
}

/// Removes every remembered entry for the provided device name.
///
/// This is used when a device must no longer be managed by the Bluetooth
/// auto-connector at all, for example after it becomes a system-paired
/// device that should only reconnect through the system connector.
static Future<void> forgetAllDeviceNameOccurrences(
SharedPreferences prefs,
String deviceName,
) async {
final normalizedName = deviceName.trim();
if (normalizedName.isEmpty) {
return;
}

final names = readRememberedDeviceNames(prefs);
final updatedNames =
names.where((name) => name != normalizedName).toList(growable: false);
if (updatedNames.length == names.length) {
return;
}

final success = await prefs.setStringList(
connectedDeviceNamesKey,
updatedNames,
);
if (success) {
_setRememberedDeviceNames(updatedNames);
_changesController.add(null);
}
}
Expand All @@ -128,4 +196,14 @@ class AutoConnectPreferences {
}
_autoConnectEnabledNotifier.value = enabled;
}

/// Updates the cached remembered device names for listening widgets.
static void _setRememberedDeviceNames(List<String> deviceNames) {
if (listEquals(_rememberedDeviceNamesNotifier.value, deviceNames)) {
return;
}
_rememberedDeviceNamesNotifier.value = List<String>.unmodifiable(
List<String>.from(deviceNames),
);
}
}
33 changes: 33 additions & 0 deletions open_wearable/lib/models/auto_connector/auto_connector.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:open_earable_flutter/open_earable_flutter.dart' hide logger;
import 'package:open_wearable/models/logger.dart';
import 'package:open_wearable/models/wearable_connector.dart';

/// Base lifecycle contract for background connection orchestrators.
abstract class AutoConnector {
/// Shared wearable connection facade used by subclasses.
final WearableConnector _connector;

AutoConnector(WearableConnector connector) : _connector = connector;

/// Broadcast connection lifecycle events emitted by the shared connector.
Stream<WearableEvent> get events => _connector.events;

/// Starts the connector lifecycle.
void start();

/// Stops the connector lifecycle.
void stop();

/// Returns whether the provided device id is already paired at the OS level.
Future<bool> isSystemDeviceId(String deviceId) {
return _connector.isSystemDeviceId(deviceId);
}

Future<Wearable> connect(DiscoveredDevice device) {
// log which auto-connector is connecting to which device
logger.i(
'AutoConnector $runtimeType connecting to device ${device.name} (${device.id})',
);
return _connector.connect(device);
}
}
Loading
Loading