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}