Commit 4973d06
Changed files (6)
app
lib
model
storage
test
app/lib/bluetooth/bluetooth_cubit.dart
@@ -23,7 +23,7 @@ class BluetoothCubit extends Cubit<BluetoothState> {
_adapterStateStateSubscription = _flutterBluePlus.adapterState.listen(_onAdapterStateChanged);
}
- FlutterBluePlusMockable _flutterBluePlus;
+ final FlutterBluePlusMockable _flutterBluePlus;
BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
app/lib/bluetooth/device_scan_cubit.dart
@@ -0,0 +1,115 @@
+import 'dart:async';
+
+import 'package:blood_pressure_app/bluetooth/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/flutter_blue_plus_mockable.dart';
+import 'package:blood_pressure_app/model/storage/settings_store.dart';
+import 'package:collection/collection.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+
+part 'device_scan_state.dart';
+
+/// A component to search for bluetooth devices.
+///
+/// For this to work the app must have access to the bluetooth adapter
+/// ([BluetoothCubit]).
+///
+/// A device counts as recognized, when the user connected with it at least
+/// once. Recognized devices connect automatically.
+class DeviceScanCubit extends Cubit<DeviceScanState> {
+ /// Search for bluetooth devices that match the criteria or are known
+ /// ([Settings.knownBleDev]).
+ DeviceScanCubit({
+ FlutterBluePlusMockable? flutterBluePlus,
+ required this.service,
+ required this.settings,
+ }) : _flutterBluePlus = flutterBluePlus ?? FlutterBluePlusMockable(),
+ super(DeviceListLoading()) {
+ assert(!_flutterBluePlus.isScanningNow, '');
+ _startScanning();
+ }
+
+ /// Storage for known devices.
+ final Settings settings;
+
+ /// Service required from bluetooth devices.
+ final Guid service;
+
+ final FlutterBluePlusMockable _flutterBluePlus;
+
+ late StreamSubscription<List<ScanResult>> _scanResultsSubscription;
+ late StreamSubscription<bool> _isScanningSubscription;
+ bool _isScanning = false;
+
+ @override
+ Future<void> close() async {
+ await _scanResultsSubscription.cancel();
+ await _isScanningSubscription.cancel();
+ await super.close();
+ }
+
+ Future<void> _startScanning() async {
+ _scanResultsSubscription = _flutterBluePlus.scanResults
+ .listen(_onScanResult,
+ onError: _onScanError,
+ );
+ try {
+ await _flutterBluePlus.startScan(
+ withServices: [service],
+ // no timeout, the user knows best how long scanning is needed
+ );
+ } catch (e) {
+ _onScanError(e);
+ }
+ _isScanningSubscription = _flutterBluePlus.isScanning
+ .listen(_onIsScanningChanged);
+ }
+
+ void _onScanResult(List<ScanResult> devices) {
+ assert(_isScanning);
+ if(state is DeviceSelected) return;
+ final preferred = devices.firstWhereOrNull((dev) =>
+ settings.knownBleDev.contains(dev.device.advName));
+ if (preferred != null) {
+ emit(DeviceSelected(preferred.device));
+ } else if (devices.isEmpty) {
+ emit(DeviceListLoading());
+ } else if (devices.length == 1) {
+ emit(SingleDeviceAvailable(devices.first));
+ } else {
+ emit(DeviceListAvailable(devices));
+ }
+ }
+
+ void _onScanError(Object error) {
+ // TODO
+ }
+
+ void _onIsScanningChanged(bool isScanning) {
+ _isScanning = isScanning;
+ // TODO: consider restarting
+ }
+
+ /// Mark a new device as known and switch to selected device state asap.
+ Future<void> acceptDevice(BluetoothDevice device) async {
+ assert(state is! DeviceSelected);
+ try {
+ await _flutterBluePlus.stopScan();
+ } catch (e) {
+ _onScanError(e);
+ return;
+ }
+ assert(!_isScanning);
+ emit(DeviceSelected(device));
+ settings.knownBleDev.add(device.advName); // TODO: does this work?
+ }
+
+ /// Remove all known devices and start scanning again.
+ Future<void> clearKnownDevices() async {
+ settings.knownBleDev = [];
+ emit(DeviceListLoading());
+ if (!_isScanning) await _startScanning();
+ }
+
+}
app/lib/bluetooth/device_scan_state.dart
@@ -0,0 +1,38 @@
+part of 'device_scan_cubit.dart';
+
+/// Search of bluetooth devices that meet some criteria.
+@immutable
+abstract class DeviceScanState {}
+
+/// Searching for devices or a reason they are not available.
+class DeviceListLoading extends DeviceScanState {}
+
+/// A device has been selected, either automatically or by the user.
+class DeviceSelected extends DeviceScanState {
+ /// Indicate that a device has been selected.
+ DeviceSelected(this.device);
+
+ /// The selected device.
+ final BluetoothDevice device;
+}
+
+/// Multiple unrecognized devices.
+class DeviceListAvailable extends DeviceScanState {
+ /// Indicate that multiple unrecognized have been found.
+ DeviceListAvailable(this.devices);
+
+ /// All found devices.
+ final List<ScanResult> devices;
+}
+
+/// One unrecognized device has been found.
+///
+/// While not technically correct, this can be understood as a connection
+/// request the user has to accept.
+class SingleDeviceAvailable extends DeviceScanState {
+ /// Indicate that one unrecognized device has been found.
+ SingleDeviceAvailable(this.device);
+
+ /// The only found device.
+ final ScanResult device;
+}
app/lib/model/storage/settings_store.dart
@@ -1,6 +1,7 @@
import 'dart:collection';
import 'dart:convert';
+import 'package:blood_pressure_app/bluetooth/device_scan_cubit.dart';
import 'package:blood_pressure_app/model/blood_pressure/medicine/medicine.dart';
import 'package:blood_pressure_app/model/blood_pressure/pressure_unit.dart';
import 'package:blood_pressure_app/model/horizontal_graph_line.dart';
@@ -43,8 +44,9 @@ class Settings extends ChangeNotifier {
bool? useLegacyList,
bool? bottomAppBars,
List<Medicine>? medications,
- int? highestMedIndex,
PressureUnit? preferredPressureUnit,
+ List<String>? knownBleDev,
+ int? highestMedIndex,
}) {
if (accentColor != null) _accentColor = accentColor;
if (sysColor != null) _sysColor = sysColor;
@@ -68,8 +70,9 @@ class Settings extends ChangeNotifier {
if (lastVersion != null) _lastVersion = lastVersion;
if (bottomAppBars != null) _bottomAppBars = bottomAppBars;
if (medications != null) _medications.addAll(medications);
- if (highestMedIndex != null) _highestMedIndex = highestMedIndex;
if (preferredPressureUnit != null) _preferredPressureUnit = preferredPressureUnit;
+ if (highestMedIndex != null) _highestMedIndex = highestMedIndex;
+ if (knownBleDev != null) _knownBleDev = knownBleDev;
_language = language; // No check here, as null is the default as well.
}
@@ -102,7 +105,6 @@ class Settings extends ChangeNotifier {
medications: ConvertUtil.parseList<String>(map['medications'])?.map((e) =>
Medicine.fromJson(jsonDecode(e)),).toList(),
highestMedIndex: ConvertUtil.parseInt(map['highestMedIndex']),
- preferredPressureUnit: PressureUnit.decode(ConvertUtil.parseInt(map['preferredPressureUnit'])),
);
// update
@@ -148,6 +150,7 @@ class Settings extends ChangeNotifier {
'medications': medications.map(jsonEncode).toList(),
'highestMedIndex': highestMedIndex,
'preferredPressureUnit': preferredPressureUnit.encode(),
+ 'knownBleDev': knownBleDev,
};
/// Serialize the object to a restoreable string.
@@ -352,7 +355,18 @@ class Settings extends ChangeNotifier {
_preferredPressureUnit = value;
notifyListeners();
}
-
+
+ List<String> _knownBleDev = [];
+ /// Bluetooth devices that previously connected.
+ ///
+ /// The exact value that is stored here is determined in [DeviceScanCubit].
+ UnmodifiableListView<String> get knownBleDev =>
+ UnmodifiableListView(_knownBleDev);
+ set knownBleDev(List<String> value) {
+ _knownBleDev = value;
+ notifyListeners();
+ }
+
final List<Medicine> _medications = [];
/// All medications ever added.
///
app/test/model/json_serialization_test.dart
@@ -96,6 +96,7 @@ void main() {
horizontalGraphLines: [HorizontalGraphLine(Colors.blue, 1230)],
bottomAppBars: true,
medications: [mockMedicine(), mockMedicine(defaultDosis: 42)],
+ knownBleDev: ['a', 'b'],
);
final fromJson = Settings.fromJson(initial.toJson());
@@ -122,6 +123,7 @@ void main() {
expect(initial.horizontalGraphLines.first.height, fromJson.horizontalGraphLines.first.height);
expect(initial.needlePinBarWidth, fromJson.needlePinBarWidth);
expect(initial.bottomAppBars, fromJson.bottomAppBars);
+ expect(initial.knownBleDev, fromJson.knownBleDev);
expect(initial.toJson(), fromJson.toJson());
});
app/.gitignore
@@ -44,6 +44,10 @@ app.*.map.json
/android/app/release
/main.dart.incremental.dill
+# Generated files
+*.mocks.dart
+
+# Others
/l10n_errors.txt
/fastlane/metadata/android/en-US/images/phoneScreenshots/resizeAll.sh
/tmp6.csv