|
|
@ -1,8 +1,10 @@ |
|
|
import 'dart:async'; |
|
|
import 'dart:async'; |
|
|
import 'dart:io'; |
|
|
import 'dart:io'; |
|
|
|
|
|
|
|
|
|
|
|
import 'package:app_settings/app_settings.dart'; |
|
|
import 'package:device_jieli/device_jieli.dart'; |
|
|
import 'package:device_jieli/device_jieli.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
|
|
|
import 'package:flutter_blue_plus/flutter_blue_plus.dart' as fbp; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
@ -37,24 +39,68 @@ class DevicesController extends GetxController { |
|
|
/// 直接读 user 对象不响应式,所以加一个 tick。 |
|
|
/// 直接读 user 对象不响应式,所以加一个 tick。 |
|
|
final RxInt boundDevicesTick = 0.obs; |
|
|
final RxInt boundDevicesTick = 0.obs; |
|
|
|
|
|
|
|
|
|
|
|
/// 蓝牙适配器是否已开启(用于 UI 提示)。 |
|
|
|
|
|
final RxBool isBluetoothOn = true.obs; |
|
|
|
|
|
|
|
|
|
|
|
/// 蓝牙开关状态监听。 |
|
|
|
|
|
StreamSubscription<fbp.BluetoothAdapterState>? _adapterStateSub; |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
void onInit() { |
|
|
void onInit() { |
|
|
super.onInit(); |
|
|
super.onInit(); |
|
|
_bleManager = Get.find<BleManager>(); |
|
|
_bleManager = Get.find<BleManager>(); |
|
|
Future.microtask(startScan); |
|
|
_init(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
@override |
|
|
@override |
|
|
void onClose() { |
|
|
void onClose() { |
|
|
|
|
|
_adapterStateSub?.cancel(); |
|
|
_bleManager.stopScan(); |
|
|
_bleManager.stopScan(); |
|
|
super.onClose(); |
|
|
super.onClose(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _init() async { |
|
|
|
|
|
// 1. 已存在已连接的设备 → 不进扫描流程,直接进入主界面。 |
|
|
|
|
|
try { |
|
|
|
|
|
if (await _bleManager.hasActiveConnection()) { |
|
|
|
|
|
Logger.i(_tag, '已存在已连接设备,直接进入主界面'); |
|
|
|
|
|
Get.offAllNamed(Routes.home); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
Logger.w(_tag, '检查已连接设备失败: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
// 2. 监听蓝牙开关:关闭→停扫并刷新提示;从关到开→自动重新扫描。 |
|
|
|
|
|
_listenAdapterState(); |
|
|
|
|
|
// 3. 开始扫描(内部包含蓝牙开关 / 权限检查与引导)。 |
|
|
|
|
|
await startScan(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _listenAdapterState() { |
|
|
|
|
|
_adapterStateSub?.cancel(); |
|
|
|
|
|
_adapterStateSub = fbp.FlutterBluePlus.adapterState.listen((state) { |
|
|
|
|
|
final on = state == fbp.BluetoothAdapterState.on; |
|
|
|
|
|
final wasOn = isBluetoothOn.value; |
|
|
|
|
|
isBluetoothOn.value = on; |
|
|
|
|
|
if (!on) { |
|
|
|
|
|
_bleManager.stopScan(); |
|
|
|
|
|
} else if (!wasOn && !isScanning.value) { |
|
|
|
|
|
// 蓝牙刚被打开 → 自动重新扫描。 |
|
|
|
|
|
startScan(); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Future<void> startScan() async { |
|
|
Future<void> startScan() async { |
|
|
Logger.i(_tag, 'startScan 被调用'); |
|
|
Logger.i(_tag, 'startScan 被调用'); |
|
|
final granted = await _ensureBluetoothPermission(); |
|
|
// 1. 蓝牙是否开启,未开启则弹引导对话框。 |
|
|
if (!granted) { |
|
|
if (!await _ensureBluetoothOn()) { |
|
|
Get.snackbar('tip'.tr, 'pleaseGrantBluetoothPermission'.tr); |
|
|
Logger.w(_tag, '蓝牙未开启,已弹出开启引导'); |
|
|
|
|
|
return; |
|
|
|
|
|
} |
|
|
|
|
|
// 2. 蓝牙相关权限,未授权则弹引导对话框。 |
|
|
|
|
|
if (!await _ensureBluetoothPermission()) { |
|
|
|
|
|
Logger.w(_tag, '蓝牙权限不足,已弹出权限引导'); |
|
|
return; |
|
|
return; |
|
|
} |
|
|
} |
|
|
final uuids = |
|
|
final uuids = |
|
|
@ -310,6 +356,60 @@ class DevicesController extends GetxController { |
|
|
_bleManager.currentDeviceAddress == device.address; |
|
|
_bleManager.currentDeviceAddress == device.address; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// 检查蓝牙是否开启。未开启则弹出引导对话框(Android 可直接开启, |
|
|
|
|
|
/// iOS 引导去系统设置),并返回 false。 |
|
|
|
|
|
Future<bool> _ensureBluetoothOn() async { |
|
|
|
|
|
var state = fbp.FlutterBluePlus.adapterStateNow; |
|
|
|
|
|
// 首帧可能还是 unknown,等首个确定状态(带超时兜底)。 |
|
|
|
|
|
if (state == fbp.BluetoothAdapterState.unknown) { |
|
|
|
|
|
try { |
|
|
|
|
|
state = await fbp.FlutterBluePlus.adapterState |
|
|
|
|
|
.firstWhere((s) => s != fbp.BluetoothAdapterState.unknown) |
|
|
|
|
|
.timeout(const Duration(seconds: 2)); |
|
|
|
|
|
} catch (_) { |
|
|
|
|
|
state = fbp.FlutterBluePlus.adapterStateNow; |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
isBluetoothOn.value = state == fbp.BluetoothAdapterState.on; |
|
|
|
|
|
if (isBluetoothOn.value) return true; |
|
|
|
|
|
_showEnableBluetoothDialog(); |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _showEnableBluetoothDialog() { |
|
|
|
|
|
if (Get.isDialogOpen ?? false) return; |
|
|
|
|
|
Get.dialog( |
|
|
|
|
|
AlertDialog( |
|
|
|
|
|
title: Text('needEnableBluetooth'.tr), |
|
|
|
|
|
content: Text('enableBluetoothDialogContent'.tr), |
|
|
|
|
|
actions: [ |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () => Get.back(), |
|
|
|
|
|
child: Text('cancel'.tr), |
|
|
|
|
|
), |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () async { |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
// Android 可直接请求系统开启蓝牙;iOS 不允许,引导去系统设置。 |
|
|
|
|
|
if (Platform.isAndroid) { |
|
|
|
|
|
try { |
|
|
|
|
|
await fbp.FlutterBluePlus.turnOn(); |
|
|
|
|
|
return; |
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
Logger.w(_tag, 'turnOn 失败,转去系统蓝牙设置: $e'); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
await AppSettings.openAppSettings( |
|
|
|
|
|
type: AppSettingsType.bluetooth); |
|
|
|
|
|
}, |
|
|
|
|
|
child: Text('enableBluetooth'.tr), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
barrierDismissible: false, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
Future<bool> _ensureBluetoothPermission() async { |
|
|
Future<bool> _ensureBluetoothPermission() async { |
|
|
// iOS: CoreBluetooth 首次扫描时由系统自动弹出蓝牙权限对话框 |
|
|
// iOS: CoreBluetooth 首次扫描时由系统自动弹出蓝牙权限对话框 |
|
|
// (依赖 Info.plist 的 NSBluetoothAlwaysUsageDescription), |
|
|
// (依赖 Info.plist 的 NSBluetoothAlwaysUsageDescription), |
|
|
@ -324,10 +424,37 @@ class DevicesController extends GetxController { |
|
|
].request(); |
|
|
].request(); |
|
|
final scanOk = results[Permission.bluetoothScan]?.isGranted ?? false; |
|
|
final scanOk = results[Permission.bluetoothScan]?.isGranted ?? false; |
|
|
final connectOk = results[Permission.bluetoothConnect]?.isGranted ?? false; |
|
|
final connectOk = results[Permission.bluetoothConnect]?.isGranted ?? false; |
|
|
return scanOk && connectOk; |
|
|
if (scanOk && connectOk) return true; |
|
|
|
|
|
// 被拒(含永久拒绝)→ 弹引导对话框,带用户去系统设置手动开启。 |
|
|
|
|
|
_showPermissionDialog(); |
|
|
|
|
|
return false; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
Logger.error('请求蓝牙权限失败: $e'); |
|
|
Logger.error('请求蓝牙权限失败: $e'); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _showPermissionDialog() { |
|
|
|
|
|
if (Get.isDialogOpen ?? false) return; |
|
|
|
|
|
Get.dialog( |
|
|
|
|
|
AlertDialog( |
|
|
|
|
|
title: Text('needPermission'.tr), |
|
|
|
|
|
content: Text('blePermissionDialogContent'.tr), |
|
|
|
|
|
actions: [ |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () => Get.back(), |
|
|
|
|
|
child: Text('cancel'.tr), |
|
|
|
|
|
), |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
AppSettings.openAppSettings(); |
|
|
|
|
|
}, |
|
|
|
|
|
child: Text('goToSettings'.tr), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
barrierDismissible: false, |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
} |
|
|
} |
|
|
|