Skip to content
Merged
1 change: 1 addition & 0 deletions app/lib/pages/home/omiglass_ota_update.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class _OmiGlassOtaUpdateState extends State<OmiGlassOtaUpdate> {
if (provider != null) {
WidgetsBinding.instance.addPostFrameCallback((_) {
provider.setOnFirmwareUpdatePage(false);
provider.resetFirmwareUpdateState();
});
}
super.dispose();
Expand Down
2 changes: 1 addition & 1 deletion app/lib/pages/home/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class _HomePageWrapperState extends State<HomePageWrapper> {
void initState() {
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
context.read<DeviceProvider>().periodicConnect('coming from HomePageWrapper', boundDeviceOnly: true);
context.read<DeviceProvider>().initiateConnection('HomePageWrapper', boundDeviceOnly: true);
}
if (SharedPreferencesUtil().notificationsEnabled) {
NotificationService.instance.register();
Expand Down
6 changes: 3 additions & 3 deletions app/lib/pages/onboarding/find_device/found_devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class _FoundDevicesState extends State<FoundDevices> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
if (mounted) {
context.read<DeviceProvider>().periodicConnect('coming from FoundDevices');
context.read<DeviceProvider>().initiateConnection('FoundDevices');
}
});
}
Expand Down Expand Up @@ -260,8 +260,8 @@ class _FoundDevicesState extends State<FoundDevices> {
color: provider.batteryPercentage <= 25
? Colors.red
: provider.batteryPercentage <= 50
? Colors.orange
: Colors.green,
? Colors.orange
: Colors.green,
),
),
),
Expand Down
177 changes: 65 additions & 112 deletions app/lib/providers/device_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,6 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
int _lastNotifiedBatteryLevel = -1;
DateTime? _lastBatteryNotifyTime;
bool _hasLowBatteryAlerted = false;
Timer? _reconnectionTimer;
DateTime? _reconnectAt;
final int _connectionCheckSeconds = 15; // 10s periods, 5s for each scan

bool _havingNewFirmware = false;
bool get havingNewFirmware => _havingNewFirmware && pairedDevice != null && isConnected;

Expand All @@ -60,6 +56,8 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
Map<String, dynamic> get latestOmiGlassFirmwareDetails => _latestOmiGlassFirmwareDetails;

Timer? _disconnectNotificationTimer;
Timer? _discoveryTimer;
bool _manualDisconnect = false;
final Debouncer _disconnectDebouncer = Debouncer(delay: const Duration(milliseconds: 500));
final Debouncer _connectDebouncer = Debouncer(delay: const Duration(milliseconds: 100));

Expand Down Expand Up @@ -103,6 +101,7 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption

// TODO: thinh, use connection directly
Future _bleDisconnectDevice(BtDevice btDevice) async {
_manualDisconnect = true;
var connection = await ServiceManager.instance().device.ensureConnection(btDevice.id);
if (connection == null) {
return Future.value(null);
Expand Down Expand Up @@ -225,131 +224,81 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
_lastBatteryNotifyTime = null;
}

Future periodicConnect(String printer, {bool boundDeviceOnly = false}) async {
_reconnectionTimer?.cancel();

/// Kicks off a single connection attempt. Native handles auto-reconnect after this.
Future<void> initiateConnection(String caller, {bool boundDeviceOnly = false}) async {
final pairedDeviceId = SharedPreferencesUtil().btDevice.id;

// Already connected — nothing to do
if (isConnected || connectedDevice != null) return;

// Known device — use ensureConnection which creates the NativeBleTransport first,
// then connects natively. If native is already connected, it just re-notifies Dart.
if (pairedDeviceId.isNotEmpty) {
try {
await ServiceManager.instance().device.ensureConnection(pairedDeviceId, force: false);
return;
} catch (e) {
Logger.debug('periodicConnect (native): ensureConnection failed: $e, falling back to scan');
}
// No paired device (onboarding) — start periodic scanning so devices
// turned on after the page loads are still discovered.
if (pairedDeviceId.isEmpty) {
if (boundDeviceOnly) return;
_startDiscoveryScanning();
return;
}

// No paired device (onboarding) — fall through to active scanning
if (pairedDeviceId.isEmpty && boundDeviceOnly) return;

_startPollingReconnect(boundDeviceOnly: boundDeviceOnly);
}

void _startPollingReconnect({bool boundDeviceOnly = false}) {
_reconnectionTimer?.cancel();
scan(t) async {
debugPrint("Period connect seconds: $_connectionCheckSeconds, triggered timer at ${DateTime.now()}");

final deviceService = ServiceManager.instance().device;
if (deviceService is DeviceService && deviceService.isWifiSyncInProgress) {
debugPrint("Skipping BLE reconnect - WiFi sync in progress");
return;
}
if (_reconnectAt != null && _reconnectAt!.isAfter(DateTime.now())) {
return;
}
if (boundDeviceOnly && SharedPreferencesUtil().btDevice.id.isEmpty) {
t.cancel();
return;
}
Logger.debug("isConnected: $isConnected, isConnecting: $isConnecting, connectedDevice: $connectedDevice");
if ((!isConnected && connectedDevice == null)) {
if (isConnecting) {
return;
}
await scanAndConnectToDevice();
} else {
t.cancel();
}
// Known device — use ensureConnection which creates the NativeBleTransport,
// then connects natively. If native is already connected, it just re-notifies Dart.
// force: true ensures we retry even if a previous attempt left a stale connection.
try {
await ServiceManager.instance().device.ensureConnection(pairedDeviceId, force: true);
} catch (e) {
// Timeout or transport failure — native keeps trying in the background.
// NativeBleTransport's BleBridge registration persists, so auto-reconnect still works.
Logger.debug('initiateConnection ($caller): ensureConnection failed: $e');
}
}

_reconnectionTimer = Timer.periodic(Duration(seconds: _connectionCheckSeconds), scan);
scan(_reconnectionTimer);
void _startDiscoveryScanning() {
_discoveryTimer?.cancel();
_runDiscoveryScan();
_discoveryTimer = Timer.periodic(const Duration(seconds: 10), (_) => _runDiscoveryScan());
}

Future<BtDevice?> _scanConnectDevice() async {
var device = await _getConnectedDevice();
if (device != null) {
return device;
Future<void> _runDiscoveryScan() async {
if (SharedPreferencesUtil().btDevice.id.isNotEmpty || isConnected) {
_discoveryTimer?.cancel();
return;
}

final pairedDeviceId = SharedPreferencesUtil().btDevice.id;
if (pairedDeviceId.isNotEmpty) {
final deviceService = ServiceManager.instance().device;
if (deviceService is DeviceService && deviceService.status == DeviceServiceStatus.ready) {
try {
Logger.debug('Attempting direct reconnection to paired device: $pairedDeviceId');
await ServiceManager.instance().device.ensureConnection(pairedDeviceId, force: true);

// Check if connection succeeded
await Future.delayed(const Duration(seconds: 2));
device = await _getConnectedDevice();
if (device != null) {
Logger.debug('Direct reconnection successful');
return device;
}
await deviceService.discover();
} catch (e) {
Logger.debug('Direct reconnection failed: $e');
Logger.debug('_runDiscoveryScan: discover failed: $e');
}
}

await ServiceManager.instance().device.discover(desirableDeviceId: pairedDeviceId);

// Waiting for the device connected (if any)
await Future.delayed(const Duration(seconds: 2));
if (connectedDevice != null) {
return connectedDevice;
}
return null;
}

Future scanAndConnectToDevice() async {
updateConnectingStatus(true);
if (isConnected) {
if (connectedDevice == null) {
var device = await _getConnectedDevice();
if (device != null) {
await setConnectedDevice(device);
SharedPreferencesUtil().deviceName = device.name;
MixpanelManager().deviceConnected();
}
}
if (isConnected && connectedDevice != null) {
updateConnectingStatus(false);
return;
}

setIsConnected(true);
final pairedDeviceId = SharedPreferencesUtil().btDevice.id;
if (pairedDeviceId.isEmpty) {
updateConnectingStatus(false);
notifyListeners();
return;
}

// else
var device = await _scanConnectDevice();
Logger.debug('inside scanAndConnectToDevice $device in device_provider');
if (device != null) {
var cDevice = await _getConnectedDevice();
if (cDevice != null) {
await setConnectedDevice(cDevice);
try {
var connection = await ServiceManager.instance().device.ensureConnection(pairedDeviceId, force: true);
if (connection != null) {
await setConnectedDevice(connection.device);
setisDeviceStorageSupport();
SharedPreferencesUtil().deviceName = cDevice.name;
SharedPreferencesUtil().deviceName = connection.device.name;
MixpanelManager().deviceConnected();
setIsConnected(true);
}
Logger.debug('device is not null $cDevice');
} catch (e) {
Logger.debug('scanAndConnectToDevice: connection failed: $e');
}
updateConnectingStatus(false);

updateConnectingStatus(false);
notifyListeners();
}

Expand All @@ -361,15 +310,15 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
void setIsConnected(bool value) {
isConnected = value;
if (isConnected) {
_reconnectionTimer?.cancel();
_discoveryTimer?.cancel();
}
notifyListeners();
}

@override
void dispose() {
_bleBatteryLevelListener?.cancel();
_reconnectionTimer?.cancel();
_discoveryTimer?.cancel();
_disconnectDebouncer.cancel();
_connectDebouncer.cancel();
ServiceManager.instance().device.unsubscribe(this);
Expand All @@ -392,14 +341,7 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
ServiceManager.instance().wal.getSyncs().flashPage.setDevice(null);

PlatformManager.instance.crashReporter.logInfo('Omi Device Disconnected');
_disconnectNotificationTimer?.cancel();
_disconnectNotificationTimer = Timer(const Duration(seconds: 30), () {
final ctx = globalNavigatorKey.currentContext;
NotificationService.instance.createNotification(
title: ctx?.l10n.deviceDisconnectedNotificationTitle ?? 'Your Omi Device Disconnected',
body: ctx?.l10n.deviceDisconnectedNotificationBody ?? 'Please reconnect to continue using your Omi.',
);
});

MixpanelManager().deviceDisconnected();
BatteryWidgetService().updateBatteryInfo(
deviceName: SharedPreferencesUtil().deviceName,
Expand All @@ -408,9 +350,20 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
isConnected: false,
);

// Retired 1s to prevent the race condition made by standby power of ble device
Future.delayed(const Duration(seconds: 1), () {
periodicConnect('coming from onDisconnect');
if (_manualDisconnect) {
_manualDisconnect = false;
_disconnectNotificationTimer?.cancel();
return;
}

// Show a notification if still disconnected after 30 seconds.
_disconnectNotificationTimer?.cancel();
_disconnectNotificationTimer = Timer(const Duration(seconds: 30), () {
final ctx = globalNavigatorKey.currentContext;
NotificationService.instance.createNotification(
title: ctx?.l10n.deviceDisconnectedNotificationTitle ?? 'Your Omi Device Disconnected',
body: ctx?.l10n.deviceDisconnectedNotificationBody ?? 'Please reconnect to continue using your Omi.',
);
});
}

Expand Down Expand Up @@ -682,8 +635,8 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption
if (connectedDevice == null) {
return;
}
setFirmwareUpdateInProgress(true);
_bleDisconnectDevice(connectedDevice!);
_reconnectAt = DateTime.now().add(Duration(seconds: 30));
}

// Reset firmware update state when update completes or fails
Expand Down
2 changes: 1 addition & 1 deletion app/lib/providers/onboarding_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen
});

ServiceManager.instance().device.subscribe(this, this);
await deviceProvider?.periodicConnect("Come from Onboarding");
await deviceProvider?.initiateConnection("Onboarding");
}

@override
Expand Down
9 changes: 6 additions & 3 deletions app/lib/services/devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,12 @@ class DeviceService implements IDeviceService {
}

Future<void> _connectToDevice(String id) async {
// Drop existing connection first
if (_connection?.status == DeviceConnectionState.connected) {
await _connection?.disconnect();
// Clean up existing connection — disconnect if active, then dispose transport
if (_connection != null) {
if (_connection!.status == DeviceConnectionState.connected) {
await _connection!.disconnect();
}
await _connection!.transport.dispose();
}
_connection = null;

Expand Down
Loading