ble_provider.dart
1 import 'package:flutter_blue_plus/flutter_blue_plus.dart'; 2 import 'package:flutter_riverpod/flutter_riverpod.dart'; 3 4 import '../core/utils/logger.dart'; 5 import '../core/utils/platform_utils.dart'; 6 import '../services/ble/ble_service.dart'; 7 import '../services/dead_drop_service.dart'; 8 import 'contacts_provider.dart'; 9 import 'groups_provider.dart'; 10 import 'messages_provider.dart' show messagesProvider, unreadCountProvider, bytesToHex; 11 import 'transport_provider.dart' show handleProfilePictureIfNeeded; 12 import 'service_provider.dart'; 13 14 final _log = LoggerService.instance; 15 16 // ============================================================================ 17 // BLE ENABLED STATE 18 // ============================================================================ 19 20 /// Provider for whether BLE is enabled by the user. 21 /// When disabled, BLE scanning and advertising won't start. 22 /// This allows testing DHT/Relay in isolation. 23 class BleEnabledNotifier extends Notifier<bool> { 24 @override 25 bool build() => PlatformUtils.supportsBle; // Disabled on desktop 26 27 void setEnabled(bool enabled) { 28 _log.info('BLE Provider', 'BLE ${enabled ? "enabled" : "disabled"} by user'); 29 state = enabled; 30 31 final bleService = ref.read(bleServiceProvider); 32 if (!enabled) { 33 // Stop everything: scanner, advertiser, AND background scan timer 34 bleService.stopAll(); 35 } else { 36 // Resume background scanning 37 bleService.resumeBackground(); 38 } 39 } 40 41 void toggle() => setEnabled(!state); 42 } 43 44 /// Provider for whether BLE is enabled by the user 45 final bleEnabledProvider = NotifierProvider<BleEnabledNotifier, bool>( 46 BleEnabledNotifier.new, 47 ); 48 49 // ============================================================================ 50 // BLE SERVICE 51 // ============================================================================ 52 53 /// Provider for the BLE service singleton 54 final bleServiceProvider = Provider<BleService>((ref) { 55 final deadDropService = ref.watch(deadDropServiceProvider); 56 final bleService = BleService(deadDropService); 57 58 ref.onDispose(() { 59 bleService.dispose(); 60 }); 61 62 return bleService; 63 }); 64 65 /// Provider for BLE initialization state 66 final bleInitializedProvider = FutureProvider<bool>((ref) async { 67 if (!PlatformUtils.supportsBle) return false; 68 69 final bleService = ref.watch(bleServiceProvider); 70 await bleService.initialize(); 71 72 // Set up callback to refresh providers when exchange completes 73 bleService.setExchangeCompleteCallback((contactId) async { 74 _log.debug('BLE Provider', 'Exchange complete, refreshing providers'); 75 // Refresh contacts and groups list 76 ref.invalidate(contactsProvider); 77 ref.invalidate(groupsProvider); 78 // Refresh messages for this contact if we know which one 79 if (contactId != null) { 80 final contactIdHex = bytesToHex(contactId); 81 _log.debug('BLE Provider', 'Refreshing messagesProvider for $contactIdHex'); 82 await ref.read(messagesProvider(contactIdHex).notifier).refresh(); 83 84 // Check for profile picture messages received via BLE 85 try { 86 final service = DeadDropService.instance; 87 if (service.isInitialized) { 88 final messages = await service.getConversation(contactId); 89 for (final msg in messages) { 90 await handleProfilePictureIfNeeded(msg, ref); 91 } 92 } 93 } catch (e) { 94 _log.warning('BLE Provider', 'Failed to check profile pictures: $e'); 95 } 96 } else { 97 // Anonymous gossip — refresh all active message providers 98 _log.debug('BLE Provider', 'No contactId, refreshing all message providers'); 99 ref.invalidate(messagesProvider); 100 } 101 // Refresh unread count 102 ref.invalidate(unreadCountProvider); 103 }); 104 105 return bleService.isInitialized; 106 }); 107 108 /// Provider for Bluetooth adapter state 109 final bluetoothAdapterStateProvider = 110 StreamProvider<BluetoothAdapterState>((ref) { 111 final bleService = ref.watch(bleServiceProvider); 112 return bleService.adapterStateStream; 113 }); 114 115 /// Provider for whether Bluetooth is on 116 final isBluetoothOnProvider = Provider<bool>((ref) { 117 final adapterState = ref.watch(bluetoothAdapterStateProvider); 118 return adapterState.maybeWhen( 119 data: (state) => state == BluetoothAdapterState.on, 120 orElse: () => false, 121 ); 122 }); 123 124 /// Provider for BLE scanning state 125 final bleScanningProvider = StateProvider<bool>((ref) => false); 126 127 /// Provider for discovered contacts 128 final discoveredContactsProvider = 129 StreamProvider<Map<String, DiscoveredContact>>((ref) { 130 final bleService = ref.watch(bleServiceProvider); 131 return bleService.discoveredContactsStream; 132 }); 133 134 /// Notifier for managing BLE scanning 135 class BleScanNotifier extends Notifier<bool> { 136 @override 137 bool build() => false; 138 139 Future<void> startScanning() async { 140 if (state) return; // Already scanning 141 142 // Check if BLE is enabled by user 143 final bleEnabled = ref.read(bleEnabledProvider); 144 if (!bleEnabled) { 145 _log.debug('BLE Scan', 'BLE is disabled by user, not starting scan'); 146 return; 147 } 148 149 final bleService = ref.read(bleServiceProvider); 150 151 try { 152 await bleService.startScanning(); 153 state = true; 154 } catch (e) { 155 state = false; 156 rethrow; 157 } 158 } 159 160 Future<void> stopScanning() async { 161 if (!state) return; // Not scanning 162 163 final bleService = ref.read(bleServiceProvider); 164 await bleService.stopScanning(); 165 state = false; 166 } 167 168 void toggle() { 169 if (state) { 170 stopScanning(); 171 } else { 172 startScanning(); 173 } 174 } 175 } 176 177 final bleScanNotifierProvider = 178 NotifierProvider<BleScanNotifier, bool>(BleScanNotifier.new); 179 180 /// Notifier for managing BLE advertising 181 class BleAdvertiseNotifier extends Notifier<bool> { 182 @override 183 bool build() => false; 184 185 Future<void> startAdvertising() async { 186 if (state) return; // Already advertising 187 188 // Check if BLE is enabled by user 189 final bleEnabled = ref.read(bleEnabledProvider); 190 if (!bleEnabled) { 191 _log.debug('BLE Advertise', 'BLE is disabled by user, not starting advertising'); 192 return; 193 } 194 195 final bleService = ref.read(bleServiceProvider); 196 197 try { 198 await bleService.startAdvertising(); 199 state = true; 200 } catch (e) { 201 state = false; 202 rethrow; 203 } 204 } 205 206 Future<void> stopAdvertising() async { 207 if (!state) return; // Not advertising 208 209 final bleService = ref.read(bleServiceProvider); 210 await bleService.stopAdvertising(); 211 state = false; 212 } 213 214 void toggle() { 215 if (state) { 216 stopAdvertising(); 217 } else { 218 startAdvertising(); 219 } 220 } 221 } 222 223 final bleAdvertiseNotifierProvider = 224 NotifierProvider<BleAdvertiseNotifier, bool>(BleAdvertiseNotifier.new); 225 226 /// Provider for advertising state stream 227 final bleAdvertisingStateProvider = StreamProvider<bool>((ref) { 228 final bleService = ref.watch(bleServiceProvider); 229 return bleService.advertisingStateStream; 230 }); 231 232 /// Provider for BLE permissions state 233 final blePermissionsProvider = FutureProvider<bool>((ref) async { 234 final bleService = ref.watch(bleServiceProvider); 235 return bleService.hasPermissions(); 236 }); 237 238 /// Provider for active BLE sessions count 239 final bleSessionsProvider = StreamProvider<int>((ref) { 240 final bleService = ref.watch(bleServiceProvider); 241 return bleService.sessionsStream.map((sessions) => sessions.length); 242 });