main
1import 'dart:async';
2
3import 'package:blood_pressure_app/features/bluetooth/backend/bluetooth_backend.dart';
4import 'package:blood_pressure_app/features/bluetooth/logic/ble_read_cubit.dart';
5import 'package:blood_pressure_app/features/bluetooth/logic/bluetooth_cubit.dart';
6import 'package:blood_pressure_app/features/bluetooth/logic/characteristics/ble_measurement_data.dart';
7import 'package:blood_pressure_app/features/bluetooth/logic/device_scan_cubit.dart';
8import 'package:blood_pressure_app/features/bluetooth/ui/closed_bluetooth_input.dart';
9import 'package:blood_pressure_app/features/bluetooth/ui/device_selection.dart';
10import 'package:blood_pressure_app/features/bluetooth/ui/input_card.dart';
11import 'package:blood_pressure_app/features/bluetooth/ui/measurement_failure.dart';
12import 'package:blood_pressure_app/features/bluetooth/ui/measurement_multiple.dart';
13import 'package:blood_pressure_app/features/bluetooth/ui/measurement_success.dart';
14import 'package:blood_pressure_app/l10n/app_localizations.dart';
15import 'package:blood_pressure_app/logging.dart';
16import 'package:blood_pressure_app/model/storage/storage.dart';
17import 'package:flutter/material.dart';
18import 'package:flutter_bloc/flutter_bloc.dart';
19import 'package:health_data_store/health_data_store.dart';
20
21/// Class for inputting measurement through bluetooth.
22class BluetoothInput extends StatefulWidget {
23 /// Create a measurement input through bluetooth.
24 const BluetoothInput({super.key,
25 required this.onMeasurement,
26 required this.manager,
27 this.bluetoothCubit,
28 this.deviceScanCubit,
29 this.bleReadCubit,
30 });
31
32 /// Bluetooth Backend manager
33 final BluetoothManager manager;
34
35 /// Called when a measurement was received through bluetooth.
36 final void Function(BloodPressureRecord data) onMeasurement;
37
38 /// Function to customize [BluetoothCubit] creation.
39 final BluetoothCubit Function()? bluetoothCubit;
40
41 /// Function to customize [DeviceScanCubit] creation.
42 final DeviceScanCubit Function()? deviceScanCubit;
43
44 /// Function to customize [BleReadCubit] creation.
45 final BleReadCubit Function(BluetoothDevice dev)? bleReadCubit;
46
47 @override
48 State<BluetoothInput> createState() => _BluetoothInputState();
49}
50
51/// Read bluetooth input happy workflow:
52/// - build is called and renders ClosedBluetoothInput with read bluetooth input button
53/// - User clicks button, toggles _isActive
54/// - _buildActive is called, waits for device_scan_state.DeviceSelected
55/// - _buildReadDevice is called, waits for ble_read_state.BleReadSuccess
56/// - onMeasurement callback triggered
57class _BluetoothInputState extends State<BluetoothInput> with TypeLogger {
58 /// Whether the user initiated reading bluetooth input
59 bool _isActive = false;
60
61 late final BluetoothCubit _bluetoothCubit;
62 DeviceScanCubit? _deviceScanCubit;
63 BleReadCubit? _deviceReadCubit;
64
65 StreamSubscription<BluetoothState>? _bluetoothSubscription;
66
67 /// Data received from reading bluetooth values.
68 ///
69 /// Its presence indicates that this input is done.
70 BleMeasurementData? _finishedData;
71
72 @override
73 void initState() {
74 super.initState();
75 _bluetoothCubit = widget.bluetoothCubit?.call() ?? BluetoothCubit(manager: widget.manager);
76 }
77
78 @override
79 void dispose() {
80 unawaited(_bluetoothSubscription?.cancel());
81 unawaited(_bluetoothCubit.close());
82 unawaited(_deviceScanCubit?.close());
83 unawaited(_deviceReadCubit?.close());
84 super.dispose();
85 }
86
87 void _returnToIdle() async {
88 // No need to show wait in the UI.
89 if (_isActive) {
90 setState(() {
91 _isActive = false;
92 _finishedData = null;
93 });
94 }
95
96 await _deviceReadCubit?.close();
97 await _deviceScanCubit?.close();
98 await _bluetoothSubscription?.cancel();
99 _deviceReadCubit = null;
100 _deviceScanCubit = null;
101 _bluetoothSubscription = null;
102 }
103
104 // TODO(derdilla): extract logic from UI
105 @override
106 Widget build(BuildContext context) {
107 const SizeChangedLayoutNotification().dispatch(context);
108 logger.finer('build[_isActive: $_isActive, _finishedData: $_finishedData]');
109
110 if (_finishedData != null) {
111 return MeasurementSuccess(
112 onTap: _returnToIdle,
113 data: _finishedData!,
114 );
115 }
116
117 if (_isActive) {
118 return _buildActive(context);
119 }
120
121 return ClosedBluetoothInput(
122 bluetoothCubit: _bluetoothCubit,
123 onStarted: () async {
124 setState(() => _isActive = true);
125 },
126 inputInfo: () async {
127 logger.finer('build.inputInfo[mounted: ${context.mounted}]');
128 if (context.mounted) {
129 await showDialog(
130 context: context,
131 builder: (BuildContext context) => AlertDialog(
132 title: Text(AppLocalizations.of(context)!.bluetoothInput),
133 content: Text(AppLocalizations.of(context)!.aboutBleInput),
134 actions: <Widget>[
135 ElevatedButton(
136 child: Text((AppLocalizations.of(context)!.btnConfirm)),
137 onPressed: () => Navigator.of(context).pop(),
138 ),
139 ],
140 ),
141 );
142 }
143 },
144 );
145 }
146
147 /// Build widget for 'adapter ready & discovering devices from bluetooth' state
148 Widget _buildActive(BuildContext context) {
149 /// blood pressure service, see https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___s_e_r_v_i_c_e_s.html
150 const String serviceUUID = '1810';
151 /// blood pressure characterisic, see https://developer.nordicsemi.com/nRF51_SDK/nRF51_SDK_v4.x.x/doc/html/group___u_u_i_d___c_h_a_r_a_c_t_e_r_i_s_t_i_c_s.html#ga95fc99c7a99cf9d991c81027e4866936
152 const String characteristicUUID = '2A35';
153
154 _bluetoothSubscription = _bluetoothCubit.stream.listen((state) {
155 if (state is BluetoothStateReady) {
156 logger.finest('_bluetoothSubscription.listen: state=$state');
157 } else {
158 logger.finer('_bluetoothSubscription.listen: state=$state, calling _returnToIdle');
159 _returnToIdle();
160 }
161 });
162
163 final settings = context.watch<Settings>();
164 _deviceScanCubit ??= widget.deviceScanCubit?.call() ?? DeviceScanCubit(
165 manager: widget.manager,
166 service: serviceUUID,
167 settings: settings,
168 );
169
170 return BlocBuilder<DeviceScanCubit, DeviceScanState>(
171 bloc: _deviceScanCubit,
172 builder: (context, DeviceScanState state) {
173 logger.finer('DeviceScanCubit.builder deviceScanState: $state');
174 const SizeChangedLayoutNotification().dispatch(context);
175 return switch(state) {
176 DeviceListLoading() => _buildMainCard(context,
177 title: Text(AppLocalizations.of(context)!.scanningForDevices),
178 child: const CircularProgressIndicator(),
179 ),
180 DeviceListAvailable() => DeviceSelection(
181 scanResults: state.devices,
182 onAccepted: (dev) => _deviceScanCubit!.acceptDevice(dev),
183 ),
184 SingleDeviceAvailable() => DeviceSelection(
185 scanResults: [ state.device ],
186 onAccepted: (dev) => _deviceScanCubit!.acceptDevice(dev),
187 ),
188 DeviceSelected() => _buildReadDevice(state, serviceUUID: serviceUUID, characteristicUUID: characteristicUUID)
189 };
190 },
191 );
192 }
193
194 /// Build widget for 'reading characteristic value from bluetooth' state
195 Widget _buildReadDevice(DeviceSelected state, { required String serviceUUID, required String characteristicUUID }) {
196 logger.finer('_buildReadDevice: state: $state');
197 return BlocConsumer<BleReadCubit, BleReadState>(
198 bloc: () {
199 _deviceReadCubit = widget.bleReadCubit?.call(state.device) ?? BleReadCubit(
200 state.device,
201 characteristicUUID: characteristicUUID,
202 serviceUUID: serviceUUID,
203 );
204 return _deviceReadCubit;
205 }(),
206 listener: (BuildContext context, BleReadState state) {
207 if (state is BleReadSuccess) {
208 final BloodPressureRecord record = state.data.asBloodPressureRecord();
209 widget.onMeasurement(record);
210 setState(() => _finishedData = state.data);
211 }
212 },
213 builder: (BuildContext context, BleReadState state) {
214 logger.finer('BleReadCubit.builder: $state');
215 const SizeChangedLayoutNotification().dispatch(context);
216
217 return switch (state) {
218 BleReadInProgress() => _buildMainCard(context,
219 child: const CircularProgressIndicator(),
220 ),
221 BleReadFailure() => MeasurementFailure(
222 onTap: _returnToIdle,
223 reason: state.reason,
224 ),
225 BleReadMultiple() => MeasurementMultiple(
226 onClosed: _returnToIdle,
227 onSelect: (data) => _deviceReadCubit!.useMeasurement(data),
228 measurements: state.data,
229 ),
230 BleReadSuccess() => MeasurementSuccess(
231 onTap: _returnToIdle,
232 data: state.data,
233 ),
234 };
235 },
236 );
237 }
238
239 Widget _buildMainCard(BuildContext context, {
240 required Widget child,
241 Widget? title,
242 }) => InputCard(
243 onClosed: _returnToIdle,
244 title: title,
245 child: child,
246 );
247}