Commit 7ed8aad

derdilla <82763757+NobodyForNothing@users.noreply.github.com>
2024-04-23 15:41:55
implement bluetooth adapter cubit
Signed-off-by: derdilla <82763757+NobodyForNothing@users.noreply.github.com>
1 parent b8354c4
app/android/app/src/main/AndroidManifest.xml
@@ -7,7 +7,23 @@
         </intent>
 
     </queries>
-   <application
+    <!-- Tell Google Play Store that your app uses Bluetooth LE
+     Set android:required="true" if bluetooth is necessary -->
+    <uses-feature android:name="android.hardware.bluetooth_le" android:required="false" />
+
+    <!-- New Bluetooth permissions in Android 12
+    https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
+    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
+    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+
+    <!-- legacy for Android 11 or lower -->
+    <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
+    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" android:maxSdkVersion="30"/>
+
+    <!-- legacy for Android 9 or lower -->
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" android:maxSdkVersion="28" />
+    <application
         android:label="blood pressure app"
         android:name="${applicationName}"
         android:icon="@mipmap/ic_launcher"
app/android/app/proguard-rules.pro
@@ -0,0 +1,1 @@
+-keep class com.lib.flutter_blue_plus.* { *; }
\ No newline at end of file
app/lib/bluetooth/bluetooth_cubit.dart
@@ -0,0 +1,78 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:blood_pressure_app/bluetooth/flutter_blue_plus_mockable.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:permission_handler/permission_handler.dart';
+
+part 'bluetooth_state.dart';
+
+/// Availability of the devices bluetooth adapter.
+///
+/// The only state that allows using the adapter is [BluetoothReady].
+class BluetoothCubit extends Cubit<BluetoothState> {
+  /// Create a cubit connecting to the bluetooth module for availability.
+  ///
+  /// [flutterBluePlus] may be provided for testing purposes.
+  BluetoothCubit({
+    FlutterBluePlusMockable? flutterBluePlus
+  }): _flutterBluePlus = flutterBluePlus ?? FlutterBluePlusMockable(),
+        super(BluetoothInitial()) {
+    _adapterStateStateSubscription = _flutterBluePlus.adapterState.listen(_onAdapterStateChanged);
+  }
+
+  FlutterBluePlusMockable _flutterBluePlus;
+
+  BluetoothAdapterState _adapterState = BluetoothAdapterState.unknown;
+
+  late StreamSubscription<BluetoothAdapterState> _adapterStateStateSubscription;
+
+  @override
+  Future<void> close() async {
+    await _adapterStateStateSubscription.cancel();
+    await super.close();
+  }
+
+  void _onAdapterStateChanged(BluetoothAdapterState state) {
+    _adapterState = state;
+    switch (_adapterState) {
+      case BluetoothAdapterState.unavailable:
+        emit(BluetoothUnfeasible());
+      case BluetoothAdapterState.unauthorized:
+        emit(BluetoothUnauthorized());
+      case BluetoothAdapterState.on:
+        emit(BluetoothReady());
+      case BluetoothAdapterState.off:
+      case BluetoothAdapterState.turningOff:
+      case BluetoothAdapterState.turningOn:
+        emit(BluetoothDisabled());
+      case BluetoothAdapterState.unknown:
+        emit(BluetoothInitial());
+    }
+
+    /// Request the permission to connect to bluetooth devices.
+    Future<bool> requestPermission() async {
+      assert(_adapterState == BluetoothAdapterState.unauthorized, 'No need to '
+          'request permission when device unavailable or already authorized.');
+      assert(await Permission.bluetoothConnect.isGranted, 'Permissions handler'
+          'should report the same as blue_plus');
+      final permission = await Permission.bluetoothConnect.request();
+      return permission.isGranted;
+    }
+
+    /// Request to enable bluetooth on the device
+    Future<bool> enableBluetooth() async {
+      assert(state is BluetoothDisabled, 'No need to enable bluetooth when '
+          'already enabled or not known to be disabled.');
+      if (!Platform.isAndroid) return false;
+      try {
+        await _flutterBluePlus.turnOn();
+        return true;
+      } on FlutterBluePlusException {
+        return false;
+      }
+    }
+  }
+}
app/lib/bluetooth/bluetooth_state.dart
@@ -0,0 +1,25 @@
+part of 'bluetooth_cubit.dart';
+
+/// State of the devices bluetooth module.
+@immutable
+abstract class BluetoothState {}
+
+/// No information on whether bluetooth is available.
+///
+/// Options relating to bluetooth should only be shown where they don't disturb.
+class BluetoothInitial extends BluetoothState {}
+
+/// There is no way bluetooth will work (e.g. no sensor).
+///
+/// Options relating to bluetooth should not be shown.
+class BluetoothUnfeasible extends BluetoothState {}
+
+/// There is a bluetooth sensor but the app has no permission.
+class BluetoothUnauthorized extends BluetoothState {}
+
+/// The device has Bluetooth and the app has permissions, but it is disabled in
+/// the device settings.
+class BluetoothDisabled extends BluetoothState {}
+
+/// Bluetooth is ready for use by the app.
+class BluetoothReady extends BluetoothState {}
app/lib/bluetooth/flutter_blue_plus_mockable.dart
@@ -0,0 +1,139 @@
+import 'dart:async';
+
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+
+/// Wrapper for FlutterBluePlus in order to easily mock it
+/// Wraps all calls for testing purposes
+class FlutterBluePlusMockable {
+  LogLevel get logLevel => FlutterBluePlus.logLevel;
+
+  /// Checks whether the hardware supports Bluetooth
+  Future<bool> get isSupported => FlutterBluePlus.isSupported;
+
+  /// The current adapter state
+  BluetoothAdapterState get adapterStateNow => FlutterBluePlus.adapterStateNow;
+
+  /// Return the friendly Bluetooth name of the local Bluetooth adapter
+  Future<String> get adapterName => FlutterBluePlus.adapterName;
+
+  /// returns whether we are scanning as a stream
+  Stream<bool> get isScanning => FlutterBluePlus.isScanning;
+
+  /// are we scanning right now?
+  bool get isScanningNow => FlutterBluePlus.isScanningNow;
+
+  /// the most recent scan results
+  List<ScanResult> get lastScanResults => FlutterBluePlus.lastScanResults;
+
+  /// a stream of scan results
+  /// - if you re-listen to the stream it re-emits the previous results
+  /// - the list contains all the results since the scan started
+  /// - the returned stream is never closed.
+  Stream<List<ScanResult>> get scanResults => FlutterBluePlus.scanResults;
+
+  /// This is the same as scanResults, except:
+  /// - it *does not* re-emit previous results after scanning stops.
+  Stream<List<ScanResult>> get onScanResults => FlutterBluePlus.onScanResults;
+
+  /// Get access to all device event streams
+  BluetoothEvents get events => FlutterBluePlus.events;
+
+  /// Gets the current state of the Bluetooth module
+  Stream<BluetoothAdapterState> get adapterState =>
+      FlutterBluePlus.adapterState;
+
+  /// Retrieve a list of devices currently connected to your app
+  List<BluetoothDevice> get connectedDevices =>
+      FlutterBluePlus.connectedDevices;
+
+  /// Retrieve a list of devices currently connected to the system
+  /// - The list includes devices connected to by *any* app
+  /// - You must still call device.connect() to connect them to *your app*
+  Future<List<BluetoothDevice>> get systemDevices =>
+      FlutterBluePlus.systemDevices;
+
+  /// Retrieve a list of bonded devices (Android only)
+  Future<List<BluetoothDevice>> get bondedDevices =>
+      FlutterBluePlus.bondedDevices;
+
+  /// Set configurable options
+  ///   - [showPowerAlert] Whether to show the power alert (iOS & MacOS only). i.e. CBCentralManagerOptionShowPowerAlertKey
+  ///       To set this option you must call this method before any other method in this package.
+  ///       See: https://developer.apple.com/documentation/corebluetooth/cbcentralmanageroptionshowpoweralertkey
+  ///       This option has no effect on Android.
+  Future<void> setOptions({
+    bool showPowerAlert = true,
+  }) => FlutterBluePlus.setOptions(showPowerAlert: showPowerAlert);
+  
+  /// Turn on Bluetooth (Android only),
+  Future<void> turnOn({int timeout = 60}) => 
+      FlutterBluePlus.turnOn(timeout: timeout);
+
+  /// Start a scan, and return a stream of results
+  /// Note: scan filters use an "or" behavior. i.e. if you set `withServices` & `withNames` we
+  /// return all the advertisments that match any of the specified services *or* any of the specified names.
+  ///   - [withServices] filter by advertised services
+  ///   - [withRemoteIds] filter for known remoteIds (iOS: 128-bit guid, android: 48-bit mac address)
+  ///   - [withNames] filter by advertised names (exact match)
+  ///   - [withKeywords] filter by advertised names (matches any substring)
+  ///   - [withMsd] filter by manfacture specific data
+  ///   - [withServiceData] filter by service data
+  ///   - [timeout] calls stopScan after a specified duration
+  ///   - [removeIfGone] if true, remove devices after they've stopped advertising for X duration
+  ///   - [continuousUpdates] If `true`, we continually update 'lastSeen' & 'rssi' by processing
+  ///        duplicate advertisements. This takes more power. You typically should not use this option.
+  ///   - [continuousDivisor] Useful to help performance. If divisor is 3, then two-thirds of advertisements are
+  ///        ignored, and one-third are processed. This reduces main-thread usage caused by the platform channel.
+  ///        The scan counting is per-device so you always get the 1st advertisement from each device.
+  ///        If divisor is 1, all advertisements are returned. This argument only matters for `continuousUpdates` mode.
+  ///   - [oneByOne] if `true`, we will stream every advertistment one by one, possibly including duplicates.
+  ///        If `false`, we deduplicate the advertisements, and return a list of devices.
+  ///   - [androidScanMode] choose the android scan mode to use when scanning
+  ///   - [androidUsesFineLocation] request `ACCESS_FINE_LOCATION` permission at runtime
+  Future<void> startScan({
+    List<Guid> withServices = const [],
+    List<String> withRemoteIds = const [],
+    List<String> withNames = const [],
+    List<String> withKeywords = const [],
+    List<MsdFilter> withMsd = const [],
+    List<ServiceDataFilter> withServiceData = const [],
+    Duration? timeout,
+    Duration? removeIfGone,
+    bool continuousUpdates = false,
+    int continuousDivisor = 1,
+    bool oneByOne = false,
+    AndroidScanMode androidScanMode = AndroidScanMode.lowLatency,
+    bool androidUsesFineLocation = false,
+  }) => FlutterBluePlus.startScan(
+    withServices: withServices,
+    withRemoteIds: withRemoteIds,
+    withNames: withNames,
+    withKeywords: withKeywords,
+    withMsd: withMsd,
+    withServiceData: withServiceData,
+    timeout: timeout,
+    removeIfGone: removeIfGone,
+    continuousUpdates: continuousUpdates,
+    continuousDivisor: continuousDivisor,
+    oneByOne: oneByOne,
+    androidScanMode: androidScanMode,
+    androidUsesFineLocation: androidUsesFineLocation,
+  );
+
+  /// Stops a scan for Bluetooth Low Energy devices
+  Future<void> stopScan() => FlutterBluePlus.stopScan();
+
+  /// Register a subscription to be canceled when scanning is complete.
+  /// This function simplifies cleanup, to prevent creating duplicate stream subscriptions.
+  ///   - this is an optional convenience function
+  ///   - prevents accidentally creating duplicate subscriptions before each scan
+  void cancelWhenScanComplete(StreamSubscription subscription) =>
+      FlutterBluePlus.cancelWhenScanComplete(subscription);
+
+  /// Sets the internal FlutterBlue log level
+  Future<void> setLogLevel(LogLevel level, {bool color = true}) =>
+      FlutterBluePlus.setLogLevel(level, color: color);
+
+  /// Request Bluetooth PHY support
+  Future<PhySupport> getPhySupport() => FlutterBluePlus.getPhySupport();
+}
\ No newline at end of file
app/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,12 +5,14 @@
 import FlutterMacOS
 import Foundation
 
+import flutter_blue_plus
 import package_info_plus
 import shared_preferences_foundation
 import sqflite
 import url_launcher_macos
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+  FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin"))
   FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
   SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
app/test/bluetooth/bluetooth_cubit_test.dart
@@ -0,0 +1,42 @@
+import 'dart:async';
+
+import 'package:blood_pressure_app/bluetooth/bluetooth_cubit.dart';
+import 'package:blood_pressure_app/bluetooth/flutter_blue_plus_mockable.dart';
+import 'package:flutter_blue_plus/flutter_blue_plus.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:mockito/annotations.dart';
+import 'package:mockito/mockito.dart';
+
+@GenerateNiceMocks([MockSpec<FlutterBluePlusMockable>()])
+import 'bluetooth_cubit_test.mocks.dart';
+
+void main() {
+  test('should translate adapter stream to state', () async {
+    final bluePlus = MockFlutterBluePlusMockable();
+    when(bluePlus.adapterState).thenAnswer((_) =>
+      Stream.fromIterable([
+        BluetoothAdapterState.unknown,
+        BluetoothAdapterState.unavailable,
+        BluetoothAdapterState.turningOff,
+        BluetoothAdapterState.off,
+        BluetoothAdapterState.unauthorized,
+        BluetoothAdapterState.turningOn,
+        BluetoothAdapterState.on,
+    ]));
+    final cubit = BluetoothCubit(flutterBluePlus: bluePlus);
+    expect(cubit.state, isA<BluetoothInitial>());
+
+    await expectLater(cubit.stream, emitsInOrder([
+      isA<BluetoothInitial>(),
+      isA<BluetoothUnfeasible>(),
+      isA<BluetoothDisabled>(),
+      isA<BluetoothDisabled>(),
+      isA<BluetoothUnauthorized>(),
+      isA<BluetoothDisabled>(),
+      isA<BluetoothReady>(),
+    ]));
+  });
+  // TODO: integration tests ?
+  test('should request permissions', () async {});
+  test('should enable bluetooth', () async {});
+}
health_data_store/pubspec.yaml
@@ -4,7 +4,7 @@ version: 0.1.0+1
 publish_to: none
 
 environment:
-  sdk: "^3.3.0"
+  sdk: '>=3.0.2 <4.0.0'
 
 dependencies:
   freezed_annotation: ^2.4.1