Add Bluetooth permission and adapter state handling for Android and iOS
This commit is contained in:
@@ -2,7 +2,11 @@ import 'package:flutter/material.dart';
|
||||
|
||||
import '../models/scooter_device.dart';
|
||||
import '../scooters/apollo_scooter.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import '../services/ble_client.dart';
|
||||
import '../services/ble_permissions.dart';
|
||||
import '../services/demo_ble_client.dart';
|
||||
import '../theme.dart';
|
||||
import 'scooter_screen.dart';
|
||||
@@ -16,8 +20,14 @@ class ScanScreen extends StatefulWidget {
|
||||
State<ScanScreen> createState() => _ScanScreenState();
|
||||
}
|
||||
|
||||
class _ScanScreenState extends State<ScanScreen> {
|
||||
/// What is stopping us from scanning, if anything.
|
||||
enum _Gate { checking, permissionDenied, permissionPermanentlyDenied, bluetoothOff, unsupported, ready }
|
||||
|
||||
class _ScanScreenState extends State<ScanScreen> with WidgetsBindingObserver {
|
||||
Stream<List<ScooterDevice>>? _scan;
|
||||
_Gate _gate = _Gate.checking;
|
||||
StreamSubscription<BleAdapterState>? _adapterSub;
|
||||
bool _requesting = false;
|
||||
|
||||
/// Development path: list every BLE device so a scooter that does not
|
||||
/// advertise F1F0/F2F0 can still be selected and classified after GATT
|
||||
@@ -27,10 +37,86 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scan = widget.ble.scan();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_prepare(request: true);
|
||||
}
|
||||
|
||||
void _restart() => setState(() => _scan = widget.ble.scan());
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_adapterSub?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Re-check when coming back from system settings or the Bluetooth toggle.
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed && _gate != _Gate.ready) {
|
||||
_prepare(request: false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission first, then adapter state, then scan.
|
||||
Future<void> _prepare({required bool request}) async {
|
||||
if (_requesting) return;
|
||||
_requesting = true;
|
||||
try {
|
||||
final access = request ? await BlePermissions.request() : await BlePermissions.status();
|
||||
if (!mounted) return;
|
||||
switch (access) {
|
||||
case BleAccess.denied:
|
||||
_setGate(_Gate.permissionDenied);
|
||||
return;
|
||||
case BleAccess.permanentlyDenied:
|
||||
_setGate(_Gate.permissionPermanentlyDenied);
|
||||
return;
|
||||
case BleAccess.unsupported:
|
||||
_setGate(_Gate.unsupported);
|
||||
return;
|
||||
case BleAccess.granted:
|
||||
break;
|
||||
}
|
||||
_watchAdapter();
|
||||
} finally {
|
||||
_requesting = false;
|
||||
}
|
||||
}
|
||||
|
||||
void _watchAdapter() {
|
||||
_adapterSub ??= widget.ble.adapterState.listen((s) {
|
||||
if (!mounted) return;
|
||||
switch (s) {
|
||||
case BleAdapterState.on:
|
||||
if (_gate != _Gate.ready) {
|
||||
_setGate(_Gate.ready);
|
||||
_restart();
|
||||
}
|
||||
case BleAdapterState.off:
|
||||
case BleAdapterState.unknown:
|
||||
_setGate(_Gate.bluetoothOff);
|
||||
case BleAdapterState.unauthorized:
|
||||
_setGate(_Gate.permissionPermanentlyDenied);
|
||||
case BleAdapterState.unsupported:
|
||||
_setGate(_Gate.unsupported);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _setGate(_Gate g) {
|
||||
if (_gate == g) return;
|
||||
setState(() {
|
||||
_gate = g;
|
||||
if (g != _Gate.ready) _scan = null;
|
||||
});
|
||||
}
|
||||
|
||||
void _restart() {
|
||||
if (_gate != _Gate.ready) {
|
||||
_prepare(request: true);
|
||||
return;
|
||||
}
|
||||
setState(() => _scan = widget.ble.scan());
|
||||
}
|
||||
|
||||
Future<void> _connect(ScooterDevice device, {BleClient? ble}) async {
|
||||
setState(() => _scan = null);
|
||||
@@ -89,7 +175,9 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _scan == null
|
||||
child: _gate != _Gate.ready
|
||||
? _gateView()
|
||||
: _scan == null
|
||||
? const SizedBox.shrink()
|
||||
: StreamBuilder<List<ScooterDevice>>(
|
||||
stream: _scan,
|
||||
@@ -152,6 +240,54 @@ class _ScanScreenState extends State<ScanScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
extension on _ScanScreenState {
|
||||
Widget _gateView() {
|
||||
final android = Platform.isAndroid;
|
||||
return switch (_gate) {
|
||||
_Gate.checking => const Center(child: CircularProgressIndicator()),
|
||||
_Gate.permissionDenied => _Empty(
|
||||
icon: Icons.bluetooth_rounded,
|
||||
title: 'Bluetooth permission needed',
|
||||
message: android
|
||||
? 'OpenMotion needs the Nearby devices permission to find and talk to your scooter. '
|
||||
'Scan results are never used for location.'
|
||||
: 'OpenMotion needs Bluetooth access to find and talk to your scooter.',
|
||||
action: FilledButton(
|
||||
onPressed: () => _prepare(request: true),
|
||||
child: const Text('Allow Bluetooth'),
|
||||
),
|
||||
),
|
||||
_Gate.permissionPermanentlyDenied => _Empty(
|
||||
icon: Icons.bluetooth_disabled_rounded,
|
||||
title: 'Bluetooth permission blocked',
|
||||
message: android
|
||||
? 'Nearby devices access was denied. Enable it for OpenMotion in system settings, then come back.'
|
||||
: 'Bluetooth access is off for OpenMotion. Enable it in Settings, then come back.',
|
||||
action: FilledButton(
|
||||
onPressed: BlePermissions.openSettings,
|
||||
child: const Text('Open settings'),
|
||||
),
|
||||
),
|
||||
_Gate.bluetoothOff => _Empty(
|
||||
icon: Icons.bluetooth_disabled_rounded,
|
||||
title: 'Bluetooth is off',
|
||||
message: android
|
||||
? 'Turn on Bluetooth to scan for your scooter.'
|
||||
: 'Turn on Bluetooth in Control Center or Settings to scan for your scooter.',
|
||||
action: android
|
||||
? FilledButton(onPressed: widget.ble.turnOn, child: const Text('Turn on Bluetooth'))
|
||||
: null,
|
||||
),
|
||||
_Gate.unsupported => const _Empty(
|
||||
icon: Icons.error_outline_rounded,
|
||||
title: 'Bluetooth unavailable',
|
||||
message: 'This device does not support Bluetooth Low Energy.',
|
||||
),
|
||||
_Gate.ready => const SizedBox.shrink(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _DeviceCard extends StatelessWidget {
|
||||
const _DeviceCard({required this.device, required this.onConnect});
|
||||
|
||||
@@ -229,7 +365,8 @@ class _Empty extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
|
||||
child: Column(
|
||||
children: [
|
||||
|
||||
Reference in New Issue
Block a user