Add Bluetooth permission and adapter state handling for Android and iOS
This commit is contained in:
@@ -8,6 +8,8 @@ import 'protocol_log.dart';
|
||||
|
||||
enum BleConnectionState { disconnected, connected }
|
||||
|
||||
enum BleAdapterState { unknown, unsupported, off, on, unauthorized }
|
||||
|
||||
/// Thin boundary between OpenScooter and the underlying BLE plugin.
|
||||
///
|
||||
/// OpenScooter v1 supports EXACTLY ONE active scooter connection at a time.
|
||||
@@ -17,6 +19,13 @@ enum BleConnectionState { disconnected, connected }
|
||||
/// Protocol code never imports the BLE plugin directly, so protocol tests run
|
||||
/// with `flutter test` and a fake subclass, without hardware.
|
||||
abstract class BleClient {
|
||||
/// Current Bluetooth adapter state, emitting on change. Fakes default to on.
|
||||
Stream<BleAdapterState> get adapterState => Stream.value(BleAdapterState.on);
|
||||
|
||||
/// Asks the OS to enable Bluetooth. Only Android supports this; elsewhere it
|
||||
/// is a no-op and the UI must direct the user to system settings.
|
||||
Future<void> turnOn() async {}
|
||||
|
||||
/// Streams the current set of visible devices. Scanning starts on listen
|
||||
/// and stops when the subscription is cancelled.
|
||||
Stream<List<ScooterDevice>> scan();
|
||||
@@ -63,6 +72,27 @@ class FlutterBleClient extends BleClient {
|
||||
@override
|
||||
Stream<BleConnectionState> get connectionState => _connState.stream;
|
||||
|
||||
@override
|
||||
Stream<BleAdapterState> get adapterState =>
|
||||
fbp.FlutterBluePlus.adapterState.map((s) => switch (s) {
|
||||
fbp.BluetoothAdapterState.on => BleAdapterState.on,
|
||||
fbp.BluetoothAdapterState.off ||
|
||||
fbp.BluetoothAdapterState.turningOff ||
|
||||
fbp.BluetoothAdapterState.turningOn =>
|
||||
BleAdapterState.off,
|
||||
fbp.BluetoothAdapterState.unauthorized => BleAdapterState.unauthorized,
|
||||
fbp.BluetoothAdapterState.unavailable => BleAdapterState.unsupported,
|
||||
fbp.BluetoothAdapterState.unknown => BleAdapterState.unknown,
|
||||
});
|
||||
|
||||
@override
|
||||
Future<void> turnOn() async {
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
_log('ADAPTER turnOn requested');
|
||||
await fbp.FlutterBluePlus.turnOn();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<List<ScooterDevice>> scan() {
|
||||
late StreamController<List<ScooterDevice>> controller;
|
||||
@@ -70,12 +100,7 @@ class FlutterBleClient extends BleClient {
|
||||
|
||||
Future<void> start() async {
|
||||
try {
|
||||
var adapter = await fbp.FlutterBluePlus.adapterState.first;
|
||||
if (adapter != fbp.BluetoothAdapterState.on &&
|
||||
defaultTargetPlatform == TargetPlatform.android) {
|
||||
await fbp.FlutterBluePlus.turnOn();
|
||||
}
|
||||
adapter = await fbp.FlutterBluePlus.adapterState
|
||||
await fbp.FlutterBluePlus.adapterState
|
||||
.where((s) => s == fbp.BluetoothAdapterState.on)
|
||||
.first
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
/// Outcome of a Bluetooth permission check or request.
|
||||
enum BleAccess {
|
||||
/// Everything granted. Bluetooth adapter state is checked separately.
|
||||
granted,
|
||||
|
||||
/// The user declined but can be asked again.
|
||||
denied,
|
||||
|
||||
/// The user declined with "don't ask again" (Android) or in Settings (iOS).
|
||||
/// Only the system settings screen can fix this.
|
||||
permanentlyDenied,
|
||||
|
||||
/// The platform reported Bluetooth as unavailable.
|
||||
unsupported,
|
||||
}
|
||||
|
||||
/// Requests the runtime permissions OpenMotion needs to scan and connect.
|
||||
///
|
||||
/// Android 12+ (API 31): BLUETOOTH_SCAN and BLUETOOTH_CONNECT, shown to the
|
||||
/// user as "Nearby devices". Android 11 and below: legacy BLE scanning needs
|
||||
/// location while in use. iOS: the single Bluetooth permission, which the OS
|
||||
/// prompts for the first time CoreBluetooth is touched.
|
||||
class BlePermissions {
|
||||
BlePermissions._();
|
||||
|
||||
static int? _androidSdk;
|
||||
|
||||
static Future<int> androidSdk() async {
|
||||
if (!Platform.isAndroid) return 0;
|
||||
return _androidSdk ??= (await DeviceInfoPlugin().androidInfo).version.sdkInt;
|
||||
}
|
||||
|
||||
static Future<List<Permission>> _needed() async {
|
||||
if (Platform.isAndroid) {
|
||||
final sdk = await androidSdk();
|
||||
return sdk >= 31
|
||||
? const [Permission.bluetoothScan, Permission.bluetoothConnect]
|
||||
: const [Permission.locationWhenInUse];
|
||||
}
|
||||
if (Platform.isIOS || Platform.isMacOS) return const [Permission.bluetooth];
|
||||
return const [];
|
||||
}
|
||||
|
||||
/// Checks without prompting.
|
||||
static Future<BleAccess> status() async {
|
||||
final statuses = <PermissionStatus>[];
|
||||
for (final p in await _needed()) {
|
||||
statuses.add(await p.status);
|
||||
}
|
||||
return _combine(statuses);
|
||||
}
|
||||
|
||||
/// Prompts for anything not yet granted.
|
||||
static Future<BleAccess> request() async {
|
||||
final needed = await _needed();
|
||||
if (needed.isEmpty) return BleAccess.granted;
|
||||
final results = await needed.request();
|
||||
return _combine(results.values.toList());
|
||||
}
|
||||
|
||||
static BleAccess _combine(List<PermissionStatus> statuses) {
|
||||
if (statuses.any((s) => s.isPermanentlyDenied || s.isRestricted)) {
|
||||
return BleAccess.permanentlyDenied;
|
||||
}
|
||||
if (statuses.any((s) => s.isDenied)) return BleAccess.denied;
|
||||
return BleAccess.granted;
|
||||
}
|
||||
|
||||
static Future<bool> openSettings() => openAppSettings();
|
||||
}
|
||||
Reference in New Issue
Block a user