Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.friend.ios.ble

Check warning on line 1 in app/android/app/src/main/kotlin/com/friend/ios/ble/OmiBleForegroundService.kt

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/android/app/src/main/kotlin/com/friend/ios/ble/OmiBleForegroundService.kt is 945 lines; consider splitting files over 800 lines.

import com.friend.ios.BleDeviceDiagnostics
import com.friend.ios.BleDisconnectEvent
Expand Down Expand Up @@ -35,7 +35,7 @@
* OmiBleManager is a pure GATT wrapper — it never decides when to connect or retry.
*/
@SuppressLint("MissingPermission")
class OmiBleForegroundService : Service() {

Check warning on line 38 in app/android/app/src/main/kotlin/com/friend/ios/ble/OmiBleForegroundService.kt

View workflow job for this annotation

GitHub Actions / Hygiene

Long function

class OmiBleForegroundService : Service() is 908 lines; consider extracting focused helpers over 150 lines.

companion object {
private const val TAG = "OmiBle.FgService"
Expand Down Expand Up @@ -505,6 +505,7 @@
val addr = address.uppercase()

val error = when {
status == 137 -> "pairing_lost"
status == 22 -> "paired_to_another_phone"
status != 0 -> "gatt_status_$status"
else -> null
Expand Down Expand Up @@ -538,7 +539,7 @@
val addr = address.uppercase()
val managed = managedDevices[addr] ?: return

if (isDestroying || status == -1 || !isBluetoothEnabled) return
if (isDestroying || status == -1 || status == 137 || !isBluetoothEnabled) return

managed.retryCount++
Log.i(TAG, "Retry #${managed.retryCount} for $addr in ${RECONNECT_DELAY_MS}ms (status=$status)")
Expand Down
11 changes: 7 additions & 4 deletions app/ios/Runner/Ble/OmiBleManager.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import CoreBluetooth

Check warning on line 1 in app/ios/Runner/Ble/OmiBleManager.swift

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/ios/Runner/Ble/OmiBleManager.swift is 826 lines; consider splitting files over 800 lines.
import Flutter
import UIKit

Expand Down Expand Up @@ -363,6 +363,7 @@
case .connectionTimeout: return "connection_timeout"
case .peripheralDisconnected: return "remote_device_terminated"
case .connectionFailed: return "connection_failed_instant_passed"
case .peerRemovedPairingInformation: return "pairing_lost"
default: return "gatt_error_\(cbError.code.rawValue)"
}
}
Expand Down Expand Up @@ -609,6 +610,7 @@
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
let uuid = peripheralUuidString(peripheral)
let isManual = manuallyDisconnected.contains(uuid)
let pairingLost = (error as? CBError)?.code == .peerRemovedPairingInformation
NSLog("[OmiBle] didFailToConnect: \(peripheral.name ?? "<nil>"), uuid=\(uuid), error=\(error?.localizedDescription ?? "nil")")
cleanupPeripheral(uuid)

Expand All @@ -625,11 +627,11 @@
incrementFailToConnectCount(uuid: uuid)
}

flutterApi?.onPeripheralDisconnected(peripheralUuid: uuid, error: error?.localizedDescription) { _ in }
flutterApi?.onPeripheralDisconnected(peripheralUuid: uuid, error: pairingLost ? "pairing_lost" : error?.localizedDescription) { _ in }

// Retry previously-connected peripherals — otherwise a failed connect silently
// drops the user. iOS queues this at the chipset level; it's free while waiting.
if !isManual, everConnected.contains(uuid) {
if !isManual, !pairingLost, everConnected.contains(uuid) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
guard let self = self else { return }
self.centralManager.connect(peripheral, options: nil)
Expand All @@ -640,6 +642,7 @@
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
let uuid = peripheralUuidString(peripheral)
let isManual = manuallyDisconnected.contains(uuid)
let pairingLost = (error as? CBError)?.code == .peerRemovedPairingInformation
NSLog("[OmiBle] didDisconnect: \(peripheral.name ?? "<nil>"), uuid=\(uuid), error=\(error?.localizedDescription ?? "nil")")
cleanupPeripheral(uuid)

Expand All @@ -663,10 +666,10 @@
}
connectionStartTimes.removeValue(forKey: uuid)

flutterApi?.onPeripheralDisconnected(peripheralUuid: uuid, error: error?.localizedDescription) { _ in }
flutterApi?.onPeripheralDisconnected(peripheralUuid: uuid, error: pairingLost ? "pairing_lost" : error?.localizedDescription) { _ in }

// Auto-reconnect unless manually disconnected
if !isManual {
if !isManual, !pairingLost {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) { [weak self] in
guard let self = self else { return }
// iOS handles this at the BLE chipset level — zero CPU/radio cost while waiting
Expand Down
25 changes: 25 additions & 0 deletions app/lib/providers/device_provider.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:async';

Check warning on line 1 in app/lib/providers/device_provider.dart

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/lib/providers/device_provider.dart is 905 lines; consider splitting files over 800 lines.
import 'dart:io';

import 'package:flutter/foundation.dart';
Expand All @@ -17,6 +17,7 @@
import 'package:omi/services/devices.dart';
import 'package:omi/services/devices/connectors/device_connection.dart';
import 'package:omi/services/devices/connectors/omi_connection.dart';
import 'package:omi/services/bridges/ble_bridge.dart';
import 'package:omi/services/notifications.dart';
import 'package:omi/services/services.dart';
import 'package:omi/services/battery_widget_service.dart';
Expand Down Expand Up @@ -65,6 +66,7 @@
// Track firmware update state to prevent showing dialog during updates
bool _isCheckingFirmware = false;
bool _isFirmwareDialogShowing = false;
bool _pairingLostDialogShowing = false;
bool _isFirmwareUpdateInProgress = false;
bool get isFirmwareUpdateInProgress => _isFirmwareUpdateInProgress;

Expand All @@ -91,6 +93,26 @@
DeviceProvider({BleDiagnosticsLoader? bleDiagnosticsLoader})
: _bleDiagnosticsLoader = bleDiagnosticsLoader ?? BleHostApi().getDeviceDiagnostics {
ServiceManager.instance().device.subscribe(this, this);
BleBridge.instance.pairingLostCallback = _showPairingLostDialog;
}

void _showPairingLostDialog() {
if (_pairingLostDialogShowing) return;
final context = globalNavigatorKey.currentContext;
if (context == null || !context.mounted) return;

_pairingLostDialogShowing = true;
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) => ConfirmationDialog(
title: dialogContext.l10n.bluetooth,
description: dialogContext.l10n.deviceUnpairedMessage,
confirmText: dialogContext.l10n.gotIt,
onConfirm: () => Navigator.of(dialogContext).pop(),
onCancel: () {},
),
).whenComplete(() => _pairingLostDialogShowing = false);
}

void setProviders(CaptureProvider provider, LocalRecordingsProvider recordingsProvider) {
Expand Down Expand Up @@ -432,6 +454,9 @@

@override
void dispose() {
if (BleBridge.instance.pairingLostCallback == _showPairingLostDialog) {
BleBridge.instance.pairingLostCallback = null;
}
_bleBatteryLevelListener?.cancel();
_bleChargingStatusListener?.cancel();
_discoveryTimer?.cancel();
Expand Down
2 changes: 2 additions & 0 deletions app/lib/services/bridges/ble_bridge.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class BleBridge implements BleFlutterApi {
void Function(String state)? bluetoothStateChangedCallback;
void Function(BlePeripheral peripheral)? peripheralDiscoveredCallback;
void Function(List<String> peripheralUuids)? stateRestoredCallback;
VoidCallback? pairingLostCallback;

final List<void Function(String fileName)> _batchRecordingFinalizedListeners = [];

Expand Down Expand Up @@ -86,6 +87,7 @@ class BleBridge implements BleFlutterApi {
void onPeripheralDisconnected(String peripheralUuid, String? error) {
final key = peripheralUuid.toUpperCase();
_disconnectCallbacks[key]?.call(false, error);
if (error == 'pairing_lost') pairingLostCallback?.call();
}

@override
Expand Down
16 changes: 16 additions & 0 deletions app/test/services/bridges/ble_bridge_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:omi/services/bridges/ble_bridge.dart';

void main() {
test('notifies the pairing-lost callback only for the stable native token', () {
final bridge = BleBridge.instance;
var notifications = 0;
bridge.pairingLostCallback = () => notifications++;
addTearDown(() => bridge.pairingLostCallback = null);

bridge.onPeripheralDisconnected('device', 'gatt_status_133');
bridge.onPeripheralDisconnected('device', 'pairing_lost');

expect(notifications, 1);
});
}
Loading