/ app / lib / providers / ble_provider.dart
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  });