Commit 4fa74eb

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-03-30 23:54:02
implement ble UI
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent 4ae1fef
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: