Commit 91fbcf9

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-06-19 09:36:16
fix deadlock in bluetooth_input closing
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 0e8c23a
Changed files (2)
app
lib
test
app/lib/components/bluetooth_input.dart
@@ -22,6 +22,9 @@ class BluetoothInput extends StatefulWidget {
   const BluetoothInput({super.key,
     required this.settings,
     required this.onMeasurement,
+    this.bluetoothCubit,
+    this.deviceScanCubit,
+    this.bleReadCubit,
   });
 
   /// Settings to store known devices.
@@ -30,6 +33,15 @@ class BluetoothInput extends StatefulWidget {
   /// Called when a measurement was received through bluetooth.
   final void Function(BloodPressureRecord data) onMeasurement;
 
+  /// Function to customize [BluetoothCubit] creation.
+  final BluetoothCubit Function()? bluetoothCubit;
+
+  /// Function to customize [DeviceScanCubit] creation.
+  final DeviceScanCubit Function()? deviceScanCubit;
+
+  /// Function to customize [BleReadCubit] creation.
+  final BleReadCubit Function()? bleReadCubit;
+
   @override
   State<BluetoothInput> createState() => _BluetoothInputState();
 }
@@ -38,11 +50,18 @@ class _BluetoothInputState extends State<BluetoothInput> {
   /// Whether the user expanded bluetooth input
   bool _isActive = false;
 
-  final BluetoothCubit _bluetoothCubit =  BluetoothCubit();
-  StreamSubscription<BluetoothState>? _bluetoothSubscription;
+  late final BluetoothCubit _bluetoothCubit;
   DeviceScanCubit? _deviceScanCubit;
   BleReadCubit? _deviceReadCubit;
 
+  StreamSubscription<BluetoothState>? _bluetoothSubscription;
+
+  @override
+  void initState() {
+    super.initState();
+    _bluetoothCubit = widget.bluetoothCubit?.call() ?? BluetoothCubit();
+  }
+
   @override
   void dispose() {
     unawaited(_bluetoothSubscription?.cancel());
@@ -53,17 +72,19 @@ class _BluetoothInputState extends State<BluetoothInput> {
   }
 
   void _returnToIdle() async {
-    await _bluetoothSubscription?.cancel();
-    _bluetoothSubscription = null;
-    await _deviceScanCubit?.close();
-    _deviceScanCubit = null;
-    await _deviceReadCubit?.close();
-    _deviceReadCubit = null;
+    // No need to show wait in the UI.
     if (_isActive) {
       setState(() {
         _isActive = false;
       });
     }
+
+    await _deviceReadCubit?.close();
+    _deviceReadCubit = null;
+    await _deviceScanCubit?.close();
+    _deviceScanCubit = null;
+    await _bluetoothSubscription?.cancel();
+    _bluetoothSubscription = null;
   }
 
   Widget _buildActive(BuildContext context) {
@@ -75,7 +96,7 @@ class _BluetoothInputState extends State<BluetoothInput> {
         _returnToIdle();
       }
     });
-    _deviceScanCubit ??= DeviceScanCubit(
+    _deviceScanCubit ??= widget.deviceScanCubit?.call() ?? DeviceScanCubit(
       service: serviceUUID,
       settings: widget.settings,
     );
@@ -99,7 +120,7 @@ class _BluetoothInputState extends State<BluetoothInput> {
             // distinction
           DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
             bloc: (){
-              _deviceReadCubit = BleReadCubit(state.device,
+              _deviceReadCubit = widget.bleReadCubit?.call() ?? BleReadCubit(state.device,
                 characteristicUUID: characteristicUUID,
                 serviceUUID: serviceUUID,
               );
app/test/ui/components/bluetooth_input_test.dart
@@ -0,0 +1,110 @@
+import 'package:bloc_test/bloc_test.dart';
+import 'package:blood_pressure_app/bluetooth/ble_read_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/characteristics/ble_measurement_data.dart';
+import 'package:blood_pressure_app/bluetooth/device_scan_cubit.dart';
+import 'package:blood_pressure_app/components/bluetooth_input.dart';
+import 'package:blood_pressure_app/components/bluetooth_input/closed_bluetooth_input.dart';
+import 'package:blood_pressure_app/components/bluetooth_input/measurement_success.dart';
+import 'package:blood_pressure_app/model/blood_pressure/record.dart';
+import 'package:blood_pressure_app/model/storage/storage.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart' hide BluetoothState;
+import 'package:flutter_test/flutter_test.dart';
+
+import 'util.dart';
+
+class _MockBluetoothCubit extends MockCubit<BluetoothState>
+    implements BluetoothCubit {}
+class _MockDeviceScanCubit extends MockCubit<DeviceScanState>
+    implements DeviceScanCubit {}
+class _MockBleReadCubit extends MockCubit<BleReadState>
+    implements BleReadCubit {}
+
+void main() {
+  testWidgets('propagates successful read', (WidgetTester tester) async {
+    final bluetoothCubit = _MockBluetoothCubit();
+    whenListen(bluetoothCubit, Stream<BluetoothState>.fromIterable([BluetoothReady()]),
+      initialState: BluetoothReady());
+    final deviceScanCubit = _MockDeviceScanCubit();
+    final devScanOk = DeviceSelected(BluetoothDevice(remoteId: DeviceIdentifier('tstDev')));
+    whenListen(deviceScanCubit, Stream<DeviceScanState>.fromIterable([devScanOk]),
+      initialState: devScanOk);
+    final bleReadCubit = _MockBleReadCubit();
+    final bleReadOk = BleReadSuccess(BleMeasurementData(
+      systolic: 123,
+      diastolic: 45,
+      meanArterialPressure: 67,
+      isMMHG: true,
+      pulse: null,
+      userID: null,
+      status: null,
+      timestamp: null,
+    ));
+    whenListen(bleReadCubit, Stream<BleReadState>.fromIterable([bleReadOk]),
+      initialState: bleReadOk,
+    );
+
+    final List<BloodPressureRecord> reads = [];
+    await tester.pumpWidget(materialApp(BluetoothInput(
+      settings: Settings(),
+      onMeasurement: reads.add,
+      bluetoothCubit: () => bluetoothCubit,
+      deviceScanCubit: () => deviceScanCubit,
+      bleReadCubit: () => bleReadCubit,
+    )));
+
+    await tester.tap(find.byType(ClosedBluetoothInput));
+    await tester.pumpAndSettle();
+    expect(find.byType(ClosedBluetoothInput), findsNothing);
+
+    expect(reads, hasLength(1));
+    expect(reads, contains(isA<BloodPressureRecord>()
+      .having((p) => p.systolic, 'sys', 123)
+      .having((p) => p.diastolic, 'dia', 45)
+      .having((p) => p.pulse, 'pul', isNull),
+    ));
+  });
+
+  testWidgets('allows closing after successful read', (WidgetTester tester) async {
+    final bluetoothCubit = _MockBluetoothCubit();
+    whenListen(bluetoothCubit, Stream<BluetoothState>.fromIterable([BluetoothReady()]),
+      initialState: BluetoothReady());
+    final deviceScanCubit = _MockDeviceScanCubit();
+    final devScanOk = DeviceSelected(BluetoothDevice(remoteId: DeviceIdentifier('tstDev')));
+    whenListen(deviceScanCubit, Stream<DeviceScanState>.fromIterable([devScanOk]),
+      initialState: devScanOk);
+    final bleReadCubit = _MockBleReadCubit();
+    final bleReadOk = BleReadSuccess(BleMeasurementData(
+      systolic: 123,
+      diastolic: 45,
+      meanArterialPressure: 67,
+      isMMHG: true,
+      pulse: null,
+      userID: null,
+      status: null,
+      timestamp: null,
+    ));
+    whenListen(bleReadCubit, Stream<BleReadState>.fromIterable([bleReadOk]),
+      initialState: bleReadOk,
+    );
+
+    final List<BloodPressureRecord> reads = [];
+    await tester.pumpWidget(materialApp(BluetoothInput(
+      settings: Settings(),
+      onMeasurement: reads.add,
+      bluetoothCubit: () => bluetoothCubit,
+      deviceScanCubit: () => deviceScanCubit,
+      bleReadCubit: () => bleReadCubit,
+    )));
+
+    await tester.tap(find.byType(ClosedBluetoothInput));
+    await tester.pumpAndSettle();
+    expect(find.byType(ClosedBluetoothInput), findsNothing);
+    expect(find.byType(MeasurementSuccess), findsOneWidget);
+
+    await tester.tap(find.byIcon(Icons.close));
+    await tester.pumpAndSettle();
+    expect(find.byType(ClosedBluetoothInput), findsOneWidget);
+  });
+}