main
1import 'dart:async';
2
3import 'package:blood_pressure_app/config.dart';
4import 'package:blood_pressure_app/features/bluetooth/bluetooth_input.dart' show BluetoothInput;
5import 'package:blood_pressure_app/features/old_bluetooth/logic/ble_read_cubit.dart';
6import 'package:blood_pressure_app/features/old_bluetooth/logic/bluetooth_cubit.dart';
7import 'package:blood_pressure_app/features/old_bluetooth/logic/characteristics/ble_measurement_data.dart';
8import 'package:blood_pressure_app/features/old_bluetooth/logic/device_scan_cubit.dart';
9import 'package:blood_pressure_app/features/old_bluetooth/ui/closed_bluetooth_input.dart';
10import 'package:blood_pressure_app/features/old_bluetooth/ui/device_selection.dart';
11import 'package:blood_pressure_app/features/old_bluetooth/ui/input_card.dart';
12import 'package:blood_pressure_app/features/old_bluetooth/ui/measurement_failure.dart';
13import 'package:blood_pressure_app/features/old_bluetooth/ui/measurement_success.dart';
14import 'package:blood_pressure_app/logging.dart';
15import 'package:blood_pressure_app/model/storage/storage.dart';
16import 'package:flutter/material.dart';
17import 'package:flutter_bloc/flutter_bloc.dart';
18import 'package:flutter_blue_plus/flutter_blue_plus.dart' show Guid;
19import 'package:blood_pressure_app/l10n/app_localizations.dart';
20import 'package:health_data_store/health_data_store.dart';
21
22/// Class for inputting measurement through bluetooth.
23///
24/// This widget is superseded by [BluetoothInput].
25class OldBluetoothInput extends StatefulWidget {
26 /// Create a measurement input through bluetooth.
27 OldBluetoothInput({super.key,
28 required this.onMeasurement,
29 }) : assert(!isTestingEnvironment, "OldBluetoothInput isn't maintained in tests");
30
31 /// Called when a measurement was received through bluetooth.
32 final void Function(BloodPressureRecord data) onMeasurement;
33
34 @override
35 State<OldBluetoothInput> createState() => _OldBluetoothInputState();
36}
37
38class _OldBluetoothInputState extends State<OldBluetoothInput> with TypeLogger {
39 /// Whether the user expanded bluetooth input
40 bool _isActive = false;
41
42 late final BluetoothCubit _bluetoothCubit;
43 DeviceScanCubit? _deviceScanCubit;
44 BleReadCubit? _deviceReadCubit;
45
46 StreamSubscription<BluetoothState>? _bluetoothSubscription;
47
48 /// Data received from reading bluetooth values.
49 ///
50 /// Its presence indicates that this input is done.
51 BleMeasurementData? _finishedData;
52
53 @override
54 void initState() {
55 super.initState();
56 _bluetoothCubit = BluetoothCubit();
57 }
58
59 @override
60 void dispose() {
61 unawaited(_bluetoothSubscription?.cancel());
62 unawaited(_bluetoothCubit.close());
63 unawaited(_deviceScanCubit?.close());
64 unawaited(_deviceReadCubit?.close());
65 super.dispose();
66 }
67
68 void _returnToIdle() async {
69 // No need to show wait in the UI.
70 if (_isActive) {
71 setState(() {
72 _isActive = false;
73 _finishedData = null;
74 });
75 }
76
77 await _deviceReadCubit?.close();
78 _deviceReadCubit = null;
79 await _deviceScanCubit?.close();
80 _deviceScanCubit = null;
81 await _bluetoothSubscription?.cancel();
82 _bluetoothSubscription = null;
83 }
84
85 Widget _buildActive(BuildContext context) {
86 final Guid serviceUUID = Guid('1810');
87 final Guid characteristicUUID = Guid('2A35');
88 _bluetoothSubscription = _bluetoothCubit.stream.listen((state) {
89 if (state is! BluetoothReady) {
90 logger.finest('_OldBluetoothInputState: _bluetoothSubscription state=$state, calling _returnToIdle');
91 _returnToIdle();
92 }
93 });
94 final settings = context.watch<Settings>();
95 _deviceScanCubit ??= DeviceScanCubit(
96 service: serviceUUID,
97 settings: settings,
98 );
99 return BlocBuilder<DeviceScanCubit, DeviceScanState>(
100 bloc: _deviceScanCubit,
101 builder: (context, DeviceScanState state) {
102 logger.finest('OldBluetoothInput _OldBluetoothInputState _deviceScanCubit: $state');
103 const SizeChangedLayoutNotification().dispatch(context);
104 return switch(state) {
105 DeviceListLoading() => _buildMainCard(context,
106 title: Text(AppLocalizations.of(context)!.scanningForDevices),
107 child: const CircularProgressIndicator(),
108 ),
109 DeviceListAvailable() => DeviceSelection(
110 scanResults: state.devices,
111 onAccepted: (dev) => _deviceScanCubit!.acceptDevice(dev),
112 ),
113 SingleDeviceAvailable() => DeviceSelection(
114 scanResults: [ state.device ],
115 onAccepted: (dev) => _deviceScanCubit!.acceptDevice(dev),
116 ),
117 // distinction
118 DeviceSelected() => BlocConsumer<BleReadCubit, BleReadState>(
119 bloc: () {
120 _deviceReadCubit = BleReadCubit(
121 state.device,
122 characteristicUUID: characteristicUUID,
123 serviceUUID: serviceUUID,
124 );
125 return _deviceReadCubit;
126 }(),
127 listener: (BuildContext context, BleReadState state) {
128 if (state is BleReadSuccess) {
129 final BloodPressureRecord record = BloodPressureRecord(
130 time: state.data.timestamp ?? DateTime.now(),
131 sys: state.data.isMMHG
132 ? Pressure.mmHg(state.data.systolic.toInt())
133 : Pressure.kPa(state.data.systolic),
134 dia: state.data.isMMHG
135 ? Pressure.mmHg(state.data.diastolic.toInt())
136 : Pressure.kPa(state.data.diastolic),
137 pul: state.data.pulse?.toInt(),
138 );
139 widget.onMeasurement(record);
140 setState(() {
141 _finishedData = state.data;
142 });
143 }
144 },
145 builder: (BuildContext context, BleReadState state) {
146 logger.finest('_OldBluetoothInputState BleReadCubit: $state');
147 const SizeChangedLayoutNotification().dispatch(context);
148 return switch (state) {
149 BleReadInProgress() => _buildMainCard(context,
150 child: const CircularProgressIndicator(),
151 ),
152 BleReadFailure() => MeasurementFailure(
153 onTap: _returnToIdle,
154 ),
155 BleReadSuccess() => MeasurementSuccess(
156 onTap: _returnToIdle,
157 data: state.data,
158 ),
159 };
160 },
161 ),
162 };
163 },
164 );
165 }
166
167 @override
168 Widget build(BuildContext context) {
169 const SizeChangedLayoutNotification().dispatch(context);
170 if (_finishedData != null) {
171 return MeasurementSuccess(
172 onTap: _returnToIdle,
173 data: _finishedData!,
174 );
175 }
176 if (_isActive) return _buildActive(context);
177 return ClosedBluetoothInput(
178 bluetoothCubit: _bluetoothCubit,
179 onStarted: () async {
180 setState(() =>_isActive = true);
181 },
182 inputInfo: () async {
183 if (context.mounted) {
184 await showDialog(
185 context: context,
186 builder: (BuildContext context) => AlertDialog(
187 title: Text(AppLocalizations.of(context)!.bluetoothInput),
188 content: Text(AppLocalizations.of(context)!.aboutBleInput),
189 actions: <Widget>[
190 ElevatedButton(
191 child: Text((AppLocalizations.of(context)!.btnConfirm)),
192 onPressed: () => Navigator.of(context).pop(),
193 ),
194 ],
195 ),
196 );
197 }
198 },
199 );
200 }
201
202 Widget _buildMainCard(BuildContext context, {
203 required Widget child,
204 Widget? title,
205 }) => InputCard(
206 onClosed: _returnToIdle,
207 title: title,
208 child: child,
209 );
210}