main
1import 'dart:async';
2
3import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_device.dart';
4import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_manager.dart';
5import 'package:blood_pressure_app/logging.dart';
6import 'package:flutter/foundation.dart';
7
8/// Base class for backend device discovery implementations
9abstract class BluetoothDeviceDiscovery<BM extends BluetoothManager> with TypeLogger {
10 /// Initialize base class for device discovery implementations.
11 BluetoothDeviceDiscovery(this.manager) {
12 logger.finer('init device discovery: $this');
13 }
14
15 /// Corresponding BluetoothManager
16 final BM manager;
17
18 /// List of discovered devices
19 final Set<BluetoothDevice> _devices = {};
20
21 /// A stream that returns the discovered devices when discovering
22 Stream<List<BluetoothDevice>> get discoverStream;
23
24 StreamSubscription<List<BluetoothDevice>>? _discoverSubscription;
25
26 /// Backend implementation to start discovering
27 @protected
28 Future<void> backendStart(String serviceUuid);
29
30 /// Backend implementation to stop discovering
31 @protected
32 Future<void> backendStop();
33
34 /// Whether device discovery is running or not
35 bool _discovering = false;
36
37 /// True when already discovering devices
38 bool get isDiscovering => _discovering;
39
40 /// Start discovering for new devices
41 ///
42 /// - [serviceUuid] The service uuid to filter on when discovering devices
43 /// - [onDevices] Callback for when devices have been discovered. The
44 /// [onDevices] callback can be called multiple times, it is also always
45 /// called with the list of all discovered devices from the start of discovering
46 Future<void> start(String serviceUuid, ValueSetter<List<BluetoothDevice>> onDevices) async {
47 if (_discovering) {
48 logger.warning('Already discovering, not starting discovery again');
49 return;
50 }
51
52 // Do not remove this if, otherwise the device_scan_cubit_test will 'hang'
53 // Not sure why, it seems during testing this would close the mocked stream
54 // immediately so when adding devices through mock.sink.add they never reach
55 // the device_scan_cubit component
56 // TODO: figure out why test fails without this if
57 if (_discoverSubscription != null) {
58 await _discoverSubscription?.cancel();
59 }
60
61 _discovering = true;
62 _devices.clear();
63
64 _discoverSubscription = discoverStream.listen((newDevices) {
65 logger.finest('New devices discovered: $newDevices');
66 assert(_discovering);
67
68 // Note that there are major differences in how backends return discovered devices,
69 // f.e. FlutterBluePlus batches results itself while BluetoothLowEnergy will always
70 // return one device per listen callback.
71 // The _devices type [Set] makes sure sure we are not adding duplicate devices.
72 _devices.addAll(newDevices);
73
74 onDevices(_devices.toList());
75 }, onError: onDiscoveryError);
76
77 logger.fine('Starting discovery for devices with service: $serviceUuid');
78 await backendStart(serviceUuid);
79 }
80
81 /// Called when an error occurs during discovery
82 void onDiscoveryError(Object error) {
83 logger.severe('Starting device scan failed', error);
84 _discovering = false;
85 }
86
87 /// Stop discovering for new devices
88 Future<void> stop() async {
89 if (!_discovering) {
90 return;
91 }
92
93 logger.finer('Stopping discovery');
94 await _discoverSubscription?.cancel();
95 await backendStop();
96 _devices.clear();
97 _discovering = false;
98 }
99}