Commit 4fa74eb
Changed files (8)
app
lib
components
l10n
windows
app/lib/components/ble_input/ble_input.dart
@@ -0,0 +1,111 @@
+import 'package:blood_pressure_app/components/ble_input/ble_input_bloc.dart';
+import 'package:blood_pressure_app/components/ble_input/ble_input_events.dart';
+import 'package:blood_pressure_app/components/ble_input/ble_input_state.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+
+class BleInput extends StatelessWidget{
+ final bloc = BleInputBloc();
+
+ BleInput({super.key});
+
+ @override
+ Widget build(BuildContext context) => BlocBuilder<BleInputBloc, BleInputState>(
+ bloc: bloc,
+ builder: (BuildContext context, BleInputState state) {
+ final localizations = AppLocalizations.of(context)!;
+ return switch (state) {
+ BleInputClosed() => IconButton(
+ icon: const Icon(Icons.bluetooth),
+ onPressed: () => bloc.add(BleInputOpened()),
+ ),
+ BleInputLoadInProgress() => _buildTwoElementCard(context,
+ const CircularProgressIndicator(),
+ Text(localizations.scanningDevices),
+ ),
+ BleInputLoadFailure() => _buildTwoElementCard(context,
+ const Icon(Icons.bluetooth_disabled),
+ Text('Failed loading input devices. Ensure the app has all neccessary permissions.'),
+ onTap: () => bloc.add(BleInputOpened()),
+ ),
+ BleInputLoadSuccess() => state.availableDevices.isEmpty // TODO: card
+ ? Text('No compatible BLE GATT devices found.')
+ : ListView.builder(
+ itemCount: state.availableDevices.length,
+ itemBuilder: (context, idx) => ListTile(
+ title: Text(state.availableDevices[idx].name),
+ trailing: state.availableDevices[idx].connectable == Connectable.available
+ ? Icon(Icons.bluetooth_audio)
+ : Icon(Icons.bluetooth_disabled),
+ onTap: () => bloc.add(BleInputDeviceSelected(state.availableDevices[idx])),
+ ),
+ ),
+ BleInputPermissionFailure() => _buildTwoElementCard(context,
+ const Icon(Icons.bluetooth_disabled),
+ Text('Permissions error. Please allow all bluetooth permissions.'
+ ' You also need the location permission on pre-Android 12 devices.'),
+ onTap: () => bloc.add(BleInputOpened()),
+ ),
+ BleConnectInProgress() => _buildTwoElementCard(context,
+ const CircularProgressIndicator(),
+ Text('Connecting to bluetooth device'),
+ ),
+ BleConnectFailed() => _buildTwoElementCard(context,
+ const Icon(Icons.bluetooth_disabled),
+ Text('Connection to bluetooth device failed :('),
+ onTap: () => bloc.add(BleInputOpened()),
+ ),
+ BleConnectSuccess() => _buildTwoElementCard(context,
+ const Icon(Icons.bluetooth_connected),
+ Text('Connected to device, waiting for measurement'),
+ ),
+ BleMeasurementInProgress() => _buildTwoElementCard(context,
+ const CircularProgressIndicator(),
+ Text('Handeling incomming measurement'),
+ ),
+ BleMeasurementSuccess() => _buildTwoElementCard(context,
+ const Icon(Icons.done, color: Colors.lightGreen,),
+ Text('Recieved measurement:'
+ '\n${state.record}'
+ '\nCuff loose: ${state.cuffLoose}'
+ '\nIrregular pulse: ${state.irregularPulse}'
+ '\nBody moved: ${state.bodyMoved}'
+ '\nWrong measurement position: ${state.improperMeasurementPosition}'
+ '\nMeasurement status: ${state.measurementStatus}'
+ ),
+ ),
+ };
+ },
+ );
+ // TODO: add method for quitting
+
+ /// Wrap open connection menu in card.
+ Widget _buildMainCard(BuildContext context, Widget child) => Container(
+ decoration: BoxDecoration(
+ color: Theme.of(context).cardColor,
+ borderRadius: BorderRadius.circular(24),
+ ),
+ width: MediaQuery.of(context).size.width,
+ height: MediaQuery.of(context).size.width,
+ padding: const EdgeInsets.all(24),
+ margin: const EdgeInsets.all(8),
+ child: child,
+ );
+
+ Widget _buildTwoElementCard(
+ BuildContext context,
+ Widget top,
+ Widget bottom, {
+ void Function()? onTap,
+ }) => InkWell(
+ onTap: onTap,
+ child: _buildMainCard(context, Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [top, const SizedBox(height: 8,), bottom,],
+ ),
+ )),
+ );
+}
app/lib/components/ble_input/ble_input_bloc.dart
@@ -6,6 +6,7 @@ import 'package:blood_pressure_app/components/ble_input/measurement_characterist
import 'package:blood_pressure_app/model/blood_pressure/record.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:permission_handler/permission_handler.dart';
// TODO: docs
class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
@@ -19,6 +20,31 @@ class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
BleInputBloc(): super(BleInputClosed()) {
on<BleInputOpened>((event, emit) async {
+ /* testing widget
+ emit(BleMeasurementSuccess(BloodPressureRecord(DateTime.now(), 123, 456, 578, 'test'),
+ bodyMoved: null,
+ cuffLoose: false,
+ irregularPulse: true,
+ improperMeasurementPosition: true,
+ measurementStatus: MeasurementStatus.ok,
+ ),);
+ return;
+ */
+
+ emit(BleInputLoadInProgress());
+ if (await Permission.bluetoothConnect.isDenied) {
+ emit(BleInputPermissionFailure());
+ unawaited(Permission.bluetoothConnect.request());
+ return;
+ }
+ emit(BleInputLoadInProgress());
+ if (await Permission.bluetoothScan.isDenied) {
+ emit(BleInputPermissionFailure());
+ unawaited(Permission.bluetoothScan.request());
+ return;
+ }
+ emit(BleInputLoadInProgress());
+
try {
emit(BleInputLoadInProgress());
await _ble.initialize();
@@ -27,8 +53,9 @@ class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
_availableDevices.add(device);
return BleInputLoadSuccess(_availableDevices.toList());
},);
- } catch (e) { // TODO: ask for permission
+ } catch (e) {
// TODO: check its really this type of exception
+ print(e);
emit(BleInputLoadFailure());
}
});
@@ -108,4 +135,3 @@ class BleInputBloc extends Bloc<BleInputEvent, BleInputState> {
}
}
-// TODO: implement UI
app/lib/components/ble_input/ble_input_state.dart
@@ -3,14 +3,24 @@ import 'package:blood_pressure_app/components/ble_input/measurement_characterist
import 'package:blood_pressure_app/model/blood_pressure/record.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+/// State of a component for inputting measurements through ble devices
sealed class BleInputState {}
-/// The ble input field is inactive.
+/// The ble input field is inactive (not opened).
class BleInputClosed extends BleInputState {}
+/// Doesn't have permission for bluetooth access.
+///
+/// The UI should show a warning to allow bluetooth and potentially location
+/// permissions.
+class BleInputPermissionFailure extends BleInputState {}
+
/// Scanning for devices.
class BleInputLoadInProgress extends BleInputState {}
-/// No device available.
+/// Could not start bluetooth search.
+///
+/// Most permissions errors should be covered by [BleInputPermissionFailure] so
+/// this might not be actionable by the user.
class BleInputLoadFailure extends BleInputState {}
/// Found devices.
class BleInputLoadSuccess extends BleInputState {
app/lib/components/dialoges/add_measurement_dialoge.dart
@@ -1,5 +1,6 @@
import 'dart:math';
+import 'package:blood_pressure_app/components/ble_input/ble_input.dart';
import 'package:blood_pressure_app/components/date_time_picker.dart';
import 'package:blood_pressure_app/components/dialoges/fullscreen_dialoge.dart';
import 'package:blood_pressure_app/components/settings/settings_widgets.dart';
@@ -264,6 +265,7 @@ class _AddEntryDialogeState extends State<AddEntryDialoge> {
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8),
children: [
+ BleInput(),
if (widget.settings.allowManualTimeInput)
_buildTimeInput(localizations),
Form(
app/lib/l10n/app_en.arb
@@ -506,5 +506,7 @@
"titleInCsv": "Title in CSV",
"@titleInCsv": {},
"preferredPressureUnit": "Preferred pressure unit",
- "@preferredPressureUnit": {}
+ "@preferredPressureUnit": {},
+ "scanningDevices": "Scanning for devices...",
+ "@scanningDevices": {}
}
app/windows/flutter/generated_plugin_registrant.cc
@@ -6,9 +6,12 @@
#include "generated_plugin_registrant.h"
+#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
app/windows/flutter/generated_plugins.cmake
@@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
+ permission_handler_windows
url_launcher_windows
)
app/pubspec.lock
@@ -524,6 +524,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.10.8"
+ permission_handler:
+ dependency: "direct main"
+ description:
+ name: permission_handler
+ sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.3.1"
+ permission_handler_android:
+ dependency: transitive
+ description:
+ name: permission_handler_android
+ sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.0.5"
+ permission_handler_apple:
+ dependency: transitive
+ description:
+ name: permission_handler_apple
+ sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
+ url: "https://pub.dev"
+ source: hosted
+ version: "9.4.4"
+ permission_handler_html:
+ dependency: transitive
+ description:
+ name: permission_handler_html
+ sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.1.1"
+ permission_handler_platform_interface:
+ dependency: transitive
+ description:
+ name: permission_handler_platform_interface
+ sha256: "48d4fcf201a1dad93ee869ab0d4101d084f49136ec82a8a06ed9cfeacab9fd20"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.1"
+ permission_handler_windows:
+ dependency: transitive
+ description:
+ name: permission_handler_windows
+ sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.2.1"
petitparser:
dependency: transitive
description: