main
1import 'dart:async';
2
3import 'package:blood_pressure_app/features/old_bluetooth/logic/bluetooth_cubit.dart';
4import 'package:blood_pressure_app/features/old_bluetooth/logic/flutter_blue_plus_mockable.dart';
5import 'package:blood_pressure_app/logging.dart';
6import 'package:blood_pressure_app/model/storage/settings_store.dart';
7import 'package:collection/collection.dart';
8import 'package:flutter/foundation.dart';
9import 'package:flutter_bloc/flutter_bloc.dart';
10import 'package:flutter_blue_plus/flutter_blue_plus.dart';
11
12part 'device_scan_state.dart';
13
14/// A component to search for bluetooth devices.
15///
16/// For this to work the app must have access to the bluetooth adapter
17/// ([BluetoothCubit]).
18///
19/// A device counts as recognized, when the user connected with it at least
20/// once. Recognized devices connect automatically.
21class DeviceScanCubit extends Cubit<DeviceScanState> with TypeLogger {
22 /// Search for bluetooth devices that match the criteria or are known
23 /// ([Settings.knownBleDev]).
24 DeviceScanCubit({
25 FlutterBluePlusMockable? flutterBluePlus,
26 required this.service,
27 required this.settings,
28 }) : _flutterBluePlus = flutterBluePlus ?? FlutterBluePlusMockable(),
29 super(DeviceListLoading()) {
30 assert(!_flutterBluePlus.isScanningNow);
31 _startScanning();
32 }
33
34 /// Storage for known devices.
35 final Settings settings;
36
37 /// Service required from bluetooth devices.
38 final Guid service;
39
40 final FlutterBluePlusMockable _flutterBluePlus;
41
42 late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
43
44 @override
45 Future<void> close() async {
46 await _scanResultsSubscription.cancel();
47 try {
48 await _flutterBluePlus.stopScan();
49 } catch (e) {
50 logger.severe('Failed to stop scanning', [e]);
51 return;
52 }
53 await super.close();
54 }
55
56 Future<void> _startScanning() async {
57 _scanResultsSubscription = _flutterBluePlus.scanResults
58 .listen(_onScanResult,
59 onError: _onScanError,
60 );
61 try {
62 await _flutterBluePlus.startScan(
63 // no timeout, the user knows best how long scanning is needed
64 withServices: [ service ],
65 // Not all devices are found using this configuration (https://pub.dev/packages/flutter_blue_plus#scanning-does-not-find-my-device).
66 );
67 } catch (e) {
68 _onScanError(e);
69 }
70 }
71
72 void _onScanResult(List<ScanResult> devices) {
73 logger.finest('_onScanResult devices: $devices');
74
75 assert(devices.isEmpty || _flutterBluePlus.isScanningNow);
76 // No need to check whether the devices really support the searched
77 // characteristic as users have to select their device anyways.
78 if(state is DeviceSelected) return;
79 final preferred = devices.firstWhereOrNull((dev) =>
80 settings.knownBleDev.contains(dev.device.platformName));
81 if (preferred != null) {
82 _flutterBluePlus.stopScan()
83 .then((_) => emit(DeviceSelected(preferred.device)));
84 } else if (devices.isEmpty) {
85 emit(DeviceListLoading());
86 } else if (devices.length == 1) {
87 emit(SingleDeviceAvailable(devices.first));
88 } else {
89 emit(DeviceListAvailable(devices));
90 }
91 }
92
93 void _onScanError(Object error) {
94 logger.severe('Starting device scan failed', [ error ]);
95 }
96
97 /// Mark a new device as known and switch to selected device state asap.
98 Future<void> acceptDevice(BluetoothDevice device) async {
99 assert(state is! DeviceSelected);
100 try {
101 await _flutterBluePlus.stopScan();
102 } catch (e) {
103 _onScanError(e);
104 return;
105 }
106 assert(!_flutterBluePlus.isScanningNow);
107 emit(DeviceSelected(device));
108 final List<String> list = settings.knownBleDev.toList();
109 list.add(device.platformName);
110 settings.knownBleDev = list;
111 }
112}