From 91b3f2320ea6ea96c4b31de3fd73920740d96f4f Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 7 Jun 2025 18:59:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86):?= =?UTF-8?q?=20=E9=87=8D=E6=9E=84=E6=9D=83=E9=99=90=E5=A4=84=E7=90=86?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=B9=B6=E4=BD=BF=E7=94=A8=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=B7=A5=E5=85=B7=E7=B1=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 升级 permission_handler 依赖至 12.0.0+1 - 创建 PermissionUtil 工具类统一处理权限请求逻辑 - 在 AgentController 和 PairingController 中使用 PermissionUtil 替代原有权限检查方法 - 改进权限请求时的用户引导界面和提示信息 - 在权限设置页面集成新的权限请求流程 --- lib/core/utils/permission_util.dart | 286 +++++++++++------- .../agent/controllers/agent_controller.dart | 21 +- .../controllers/pairing_controller.dart | 71 ++++- .../settings/views/permissions_view.dart | 225 ++++++++------ pubspec.yaml | 2 +- 5 files changed, 396 insertions(+), 209 deletions(-) diff --git a/lib/core/utils/permission_util.dart b/lib/core/utils/permission_util.dart index 7b91cc246..e51aafb3f 100644 --- a/lib/core/utils/permission_util.dart +++ b/lib/core/utils/permission_util.dart @@ -1,134 +1,202 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:app_settings/app_settings.dart'; +import 'dart:developer'; +import 'logger.dart'; + +// class Logger { +// static void e(String tag, String message) { +// debugPrint('[$tag] ERROR: $message'); +// } +// } class PermissionUtil { - /// 检查并请求多个权限 - /// - /// [permissions] 要检查的权限列表,如 [Permission.microphone, Permission.location] - /// [rationaleTitle] 弹窗标题 - /// [rationaleMessage] 权限用途说明内容(支持多行) - /// - /// 返回是否所有权限都已被授予 - static Future checkAndRequestPermissions({ - required List permissions, - required String rationaleTitle, - required List rationaleMessage, + static final PermissionUtil instance = PermissionUtil._(); + PermissionUtil._(); + + OverlayEntry? _explanationOverlayEntry; + + Future requestPermission({ + required BuildContext context, + required Permission permissionType, + required String permissionName, + required String explanationText, + required String permanentDenialText, + String settingsButtonText = '前往设置', + String cancelButtonText = '取消', }) async { try { - Map statuses = {}; - - // 检查是否都已授权 - bool allGranted = true; - for (var permission in permissions) { - final status = await permission.status; - if (!status.isGranted) { - allGranted = false; - statuses[permission] = status; + final PermissionStatus status = await permissionType.status; + log('初始权限状态 for $permissionName: $status'); + + // 处理已授权状态(包括授予状态和临时通知权限) + if (status == PermissionStatus.granted || + status == PermissionStatus.provisional) { + log('$permissionName 权限已授予或临时授权。'); + return true; + } + + // 处理永久拒绝状态 + if (status == PermissionStatus.permanentlyDenied) { + log('$permissionName 权限被永久拒绝,显示引导对话框。'); + await _showPermanentDenialDialog( + context: context, + permissionName: permissionName, + permanentDenialText: permanentDenialText, + settingsButtonText: settingsButtonText, + cancelButtonText: cancelButtonText, + ); + return false; + } + + // 处理初始拒绝/未决定状态 + if (status == PermissionStatus.denied) { + log('$permissionName 权限被拒绝或未决定,显示说明横幅并请求。'); + _showExplanationOverlay( + context: context, + permissionName: permissionName, + explanationText: explanationText, + ); + + final PermissionStatus newStatus = await permissionType.request(); + log('请求后 $permissionName 的新权限状态: $newStatus'); + + _hideExplanationOverlay(); + + // 处理请求后的状态 + if (newStatus == PermissionStatus.granted || + newStatus == PermissionStatus.provisional) { + log('$permissionName 权限在请求后被授予或临时授权。'); + return true; + } else if (newStatus == PermissionStatus.permanentlyDenied) { + log('$permissionName 权限在请求后被永久拒绝,再次显示引导对话框。'); + await _showPermanentDenialDialog( + context: context, + permissionName: permissionName, + permanentDenialText: permanentDenialText, + settingsButtonText: settingsButtonText, + cancelButtonText: cancelButtonText, + ); } + return false; } - if (allGranted) return true; - - // 判断是否包含永久拒绝 - bool hasPermanentDenied = - statuses.values.any((status) => status.isPermanentlyDenied); - - // 显示提示弹窗 - _showPermissionDialog( - isPermanent: hasPermanentDenied, - title: rationaleTitle, - messages: rationaleMessage, - onConfirm: () async { - if (hasPermanentDenied) { - AppSettings.openAppSettings(); - } else { - final newStatuses = await permissions.request(); - final allGrantedNow = - newStatuses.values.every((status) => status.isGranted); - if (!allGrantedNow) { - print("部分权限被拒绝: $newStatuses"); - } - } - }, - ); + // 处理受限状态 + if (status == PermissionStatus.restricted || + status == PermissionStatus.limited) { + log('$permissionName 权限受限或受限访问: $status'); + await _showRestrictedPermissionDialog(context, permissionName); + return false; + } + // 其他未知状态处理 + log('$permissionName 处于未知状态: $status'); return false; } catch (e) { - print("权限检查异常: $e"); + Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e'); + _hideExplanationOverlay(); return false; } } - /// 权限说明弹窗 - static void _showPermissionDialog({ - required bool isPermanent, - required String title, - required List messages, - required VoidCallback onConfirm, + void _showExplanationOverlay({ + required BuildContext context, + required String permissionName, + required String explanationText, }) { - Get.generalDialog( - barrierDismissible: false, - barrierLabel: '权限提示', - transitionBuilder: (context, anim1, anim2, child) { - return SlideTransition( - position: Tween( - begin: Offset(0, -1), - end: Offset(0, 0), - ).animate(CurvedAnimation(parent: anim1, curve: Curves.easeOut)), - child: child, - ); - }, - pageBuilder: (context, anim1, anim2) { - return SafeArea( - child: Align( - alignment: Alignment.topCenter, - child: Padding( - padding: const EdgeInsets.only(top: 80, left: 24, right: 24), - child: Material( - borderRadius: BorderRadius.circular(12), - color: Colors.white, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, - style: TextStyle( - fontSize: 18, fontWeight: FontWeight.bold)), - const SizedBox(height: 12), - ...messages.map((msg) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: Text("• $msg"), - )), - const SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () => Get.back(), - child: const Text('取消'), - ), - const SizedBox(width: 8), - TextButton( - onPressed: () { - Get.back(); - onConfirm(); - }, - child: Text(isPermanent ? '前往设置' : '授权'), - ), - ], - ), - ], + _hideExplanationOverlay(); // 确保移除旧叠加层 + + _explanationOverlayEntry = OverlayEntry( + builder: (context) => Positioned( + top: MediaQuery.of(context).padding.top + 10, + left: 16, + right: 16, + child: Material( + borderRadius: BorderRadius.circular(12), + elevation: 4, + color: Colors.white, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '需要$permissionName权限', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, ), ), - ), + const SizedBox(height: 8), + Text( + explanationText, + style: const TextStyle(fontSize: 14), + ), + ], ), ), - ); - }, + ), + ), + ); + + Future.delayed(Duration.zero, () { + if (_explanationOverlayEntry != null) { + Overlay.of(context).insert(_explanationOverlayEntry!); + log('权限说明叠加层已显示。'); + } + }); + } + + void _hideExplanationOverlay() { + _explanationOverlayEntry?.remove(); + _explanationOverlayEntry = null; + } + + Future _showPermanentDenialDialog({ + required BuildContext context, + required String permissionName, + required String permanentDenialText, + required String settingsButtonText, + required String cancelButtonText, + }) async { + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text('需要$permissionName权限'), + content: Text(permanentDenialText), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: Text(cancelButtonText), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + AppSettings.openAppSettings(); + }, + child: Text(settingsButtonText), + ), + ], + ), + ); + } + + Future _showRestrictedPermissionDialog( + BuildContext context, String permissionName) async { + await showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('$permissionName权限受限'), + content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('确定'), + ), + ], + ), ); } } diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 530f498ea..1a80b8679 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -698,7 +698,16 @@ class AgentController extends GetxController { // 如果要切换到语音模式,需要先检查麦克风权限 if (isTextInputMode.value) { - final hasPermission = await _checkMicrophonePermission(); + // 使用 PermissionUtil 检查麦克风权限 + final hasPermission = await PermissionUtil.instance.requestPermission( + context: Get.context!, + permissionType: Permission.microphone, + permissionName: '麦克风', + explanationText: '语音识别与翻译功能需要访问麦克风。', + permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); if (!hasPermission) { return; // 权限未获取,保持文本模式 } @@ -871,7 +880,15 @@ class AgentController extends GetxController { if (isListening.value) return; // 检查麦克风权限 - final hasPermission = await _checkMicrophonePermission(); + final hasPermission = await PermissionUtil.instance.requestPermission( + context: Get.context!, + permissionType: Permission.microphone, + permissionName: '麦克风', + explanationText: '语音识别与翻译功能需要访问麦克风。', + permanentDenialText: '您已永久拒绝麦克风权限,请前往系统设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); if (!hasPermission) { isTextInputMode.value = true; // 如果没有权限,切回文本模式 return; diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index eaccfe86a..64ac3ea26 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -7,6 +7,7 @@ import '../../../core/utils/logger.dart'; import 'dart:io'; import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/ble_manager.dart'; +import '../../../core/utils/permission_util.dart'; /// 配对状态枚举 enum PairingState { @@ -359,7 +360,7 @@ class PairingController extends GetxController { if (success) { Logger.i(_tag, 'BLE配对启动成功,立即注册后台扫描'); - + // 配对启动成功后立即注册后台扫描 try { // Android平台注册后台扫描 @@ -372,7 +373,7 @@ class PairingController extends GetxController { Logger.e(_tag, '注册后台扫描失败: $e'); // 注册失败不影响配对流程继续 } - + Logger.i(_tag, '等待BLE连接完成...'); // 配对结果将通过BLE状态监听回调处理 } else { @@ -508,23 +509,75 @@ class PairingController extends GetxController { return true; } - // Android权限检查 - final permissions = []; - permissions.add(Permission.locationWhenInUse); + // Android权限检查 - 使用 PermissionUtil + final context = Get.context; + if (context == null) { + Logger.e(_tag, '无法获取有效上下文,跳过权限请求'); + return false; + } + + // 根据 Android 版本确定需要的权限 + List requiredPermissions = [ + Permission.locationWhenInUse, + ]; if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { - permissions.add(Permission.bluetoothScan); - permissions.add(Permission.bluetoothConnect); + requiredPermissions.add(Permission.bluetoothScan); + requiredPermissions.add(Permission.bluetoothConnect); } - final statuses = await permissions.request(); - return statuses.values.every((status) => status.isGranted); + // 逐个请求权限 + for (var permission in requiredPermissions) { + final permissionName = _getPermissionName(permission); + Logger.d(_tag, '正在请求 $permissionName 权限'); + + final granted = await PermissionUtil.instance.requestPermission( + context: context, + permissionType: permission, + permissionName: permissionName, + explanationText: _getExplanationText(permission), + permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); + + if (!granted) { + Logger.w(_tag, '$permissionName 权限未授予'); + return false; + } + } + + return true; } catch (e) { Logger.e(_tag, '权限检查失败: $e'); return false; } } + // 获取权限的用户友好名称 + String _getPermissionName(Permission permission) { + if (permission == Permission.location) return '定位'; + if (permission == Permission.locationWhenInUse) return '定位'; + if (permission == Permission.bluetoothScan) return '蓝牙扫描'; + if (permission == Permission.bluetoothConnect) return '蓝牙连接'; + return '未知权限'; + } + + // 获取权限说明文本 + String _getExplanationText(Permission permission) { + if (permission == Permission.location || + permission == Permission.locationWhenInUse) { + return '用于发现和连接蓝牙设备。我们不会在应用后台使用定位功能。'; + } + + if (permission == Permission.bluetoothScan || + permission == Permission.bluetoothConnect) { + return '用于扫描和连接蓝牙设备,这是与耳机通信所必需的权限。'; + } + + return '此权限是应用正常运行所必需的。'; + } + /// 设置蓝牙状态监听 void _setupBluetoothListener() { _bluetoothStateSubscription?.cancel(); diff --git a/lib/modules/settings/views/permissions_view.dart b/lib/modules/settings/views/permissions_view.dart index 55c01331b..ccc94bfb1 100644 --- a/lib/modules/settings/views/permissions_view.dart +++ b/lib/modules/settings/views/permissions_view.dart @@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../controllers/settings_controller.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'dart:ui'; +import 'package:deep_voice/core/utils/permission_util.dart'; // 引入PermissionUtil class PermissionsView extends GetView { const PermissionsView({Key? key}) : super(key: key); @@ -11,7 +12,7 @@ class PermissionsView extends GetView { @override Widget build(BuildContext context) { final isDarkMode = Theme.of(context).brightness == Brightness.dark; - + return Scaffold( backgroundColor: isDarkMode ? Colors.black : Colors.grey[50], appBar: AppBar( @@ -41,45 +42,53 @@ class PermissionsView extends GetView { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } - + if (snapshot.hasError) { return Center(child: Text('获取权限状态失败: ${snapshot.error}'.tr)); } - + final permissionStatuses = snapshot.data ?? {}; - + return ListView( padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), children: [ // 通讯权限组 - _buildPermissionSection('通讯权限', Icons.phone_android, [ - Permission.phone, - Permission.sms - ], permissionStatuses, isDarkMode), - + _buildPermissionSection( + '通讯权限', + Icons.phone_android, + [Permission.phone, Permission.sms], + permissionStatuses, + isDarkMode), + SizedBox(height: 20.h), - + // 日历权限组 - _buildPermissionSection('日历权限', Icons.calendar_today, [ - Permission.calendarFullAccess - ], permissionStatuses, isDarkMode), - + _buildPermissionSection( + '日历权限', + Icons.calendar_today, + [Permission.calendarFullAccess], + permissionStatuses, + isDarkMode), + SizedBox(height: 20.h), - + // 位置权限组 - _buildPermissionSection('位置权限', Icons.location_on, [ - Permission.location - ], permissionStatuses, isDarkMode), + _buildPermissionSection('位置权限', Icons.location_on, + [Permission.location], permissionStatuses, isDarkMode), ], ); }, ), ); } - + // 构建权限组区域 - Widget _buildPermissionSection(String title, IconData icon, List permissions, - Map statuses, bool isDarkMode) { + Widget _buildPermissionSection( + String title, + IconData icon, + List permissions, + Map statuses, + bool isDarkMode) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -96,7 +105,7 @@ class PermissionsView extends GetView { ), ), ), - + // 权限项容器 Container( decoration: BoxDecoration( @@ -119,7 +128,8 @@ class PermissionsView extends GetView { ? ImageFilter.blur(sigmaX: 5, sigmaY: 5) : ImageFilter.blur(sigmaX: 0, sigmaY: 0), child: Column( - children: _buildPermissionItems(permissions, statuses, isDarkMode), + children: + _buildPermissionItems(permissions, statuses, isDarkMode), ), ), ), @@ -127,52 +137,52 @@ class PermissionsView extends GetView { ], ); } - + // 构建权限项列表 - List _buildPermissionItems(List permissions, + List _buildPermissionItems(List permissions, Map statuses, bool isDarkMode) { final List items = []; - - // 获取描述文本 - String getDescription(Permission permission) { + + // 获取名称 + String getName(Permission permission) { switch (permission) { case Permission.phone: - return '用于拨打电话功能'; + return '电话'; case Permission.sms: - return '用于发送短信功能'; + return '短信'; case Permission.calendarWriteOnly: - return '用于创建日历事件和提醒'; + return '日历写入'; case Permission.calendarFullAccess: - return '用于读取和管理日历事件及提醒'; + return '日历完整访问'; case Permission.location: - return '用于获取当前位置信息'; + return '位置信息'; default: - return '应用功能需要'; + return permission.toString(); } } - - // 获取名称 - String getName(Permission permission) { + + // 获取描述文本 + String getDescription(Permission permission) { switch (permission) { case Permission.phone: - return '电话'; + return '用于拨打电话功能'; case Permission.sms: - return '短信'; + return '用于发送短信功能'; case Permission.calendarWriteOnly: - return '日历写入'; + return '用于创建日历事件和提醒'; case Permission.calendarFullAccess: - return '日历完整访问'; + return '用于读取和管理日历事件及提醒'; case Permission.location: - return '位置信息'; + return '用于获取当前位置信息'; default: - return permission.toString(); + return '应用功能需要'; } } - + for (int i = 0; i < permissions.length; i++) { final permission = permissions[i]; final status = statuses[permission] ?? PermissionStatus.denied; - + items.add( _buildPermissionItem( name: getName(permission), @@ -182,7 +192,7 @@ class PermissionsView extends GetView { isDarkMode: isDarkMode, ), ); - + // 添加分隔线,最后一个不加 if (i < permissions.length - 1) { items.add(Divider( @@ -191,10 +201,10 @@ class PermissionsView extends GetView { )); } } - + return items; } - + // 构建单个权限项 Widget _buildPermissionItem({ required String name, @@ -248,12 +258,12 @@ class PermissionsView extends GetView { ), ); } - + // 状态文本 Widget _buildStatusText(PermissionStatus status, bool isDarkMode) { String text; Color color; - + if (status.isGranted) { text = '已授权'.tr; color = Colors.green; @@ -262,7 +272,7 @@ class PermissionsView extends GetView { color = Colors.red; } else if (status.isPermanentlyDenied) { text = '需要在系统设置中开启'.tr; - color = Colors.grey; + color = Colors.orange; } else if (status.isLimited) { text = '部分授权'.tr; color = Colors.orange; @@ -270,7 +280,7 @@ class PermissionsView extends GetView { text = '未知状态'.tr; color = Colors.grey; } - + return Text( text, style: TextStyle( @@ -280,45 +290,84 @@ class PermissionsView extends GetView { ), ); } - + // 处理权限请求 - Future _handlePermissionAction(Permission permission, PermissionStatus status) async { - if (status.isPermanentlyDenied) { - // 打开应用设置 - controller.openSettings(); - return; - } - - if (!status.isGranted) { - // 请求权限 - final result = await controller.requestPermission(permission); - - if (result.isGranted) { - Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM); - } else if (result.isPermanentlyDenied) { - Get.dialog( - AlertDialog( - title: Text('需要权限'.tr), - content: Text('此权限需要在设置中手动开启'.tr), - actions: [ - TextButton( - onPressed: () => Get.back(), - child: Text('取消'.tr), - ), - TextButton( - onPressed: () { - Get.back(); - controller.openSettings(); - }, - child: Text('去设置'.tr), - ), - ], - ), + Future _handlePermissionAction( + Permission permission, PermissionStatus status) async { + try { + final context = Get.context; + if (context == null) return; + + final permissionName = _getPermissionName(permission); + final explanationText = _getExplanationText(permission); + + // 使用 PermissionUtil 请求权限 + final hasPermission = await PermissionUtil.instance.requestPermission( + context: context, + permissionType: permission, + permissionName: permissionName, + explanationText: explanationText, + permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); + + if (hasPermission) { + Get.snackbar( + '成功'.tr, + '$permissionName权限已授予'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.green, + colorText: Colors.white, ); } - - // 刷新界面 + + // 刷新界面,重新获取权限状态 Get.forceAppUpdate(); + } catch (e) { + Get.snackbar( + '错误'.tr, + '请求权限时出错: $e'.tr, + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red, + colorText: Colors.white, + ); + } + } + + // 获取权限的用户友好名称 + String _getPermissionName(Permission permission) { + switch (permission) { + case Permission.phone: + return '电话'; + case Permission.sms: + return '短信'; + case Permission.calendarWriteOnly: + return '日历写入'; + case Permission.calendarFullAccess: + return '日历完整访问'; + case Permission.location: + return '位置信息'; + default: + return permission.toString(); + } + } + + // 获取权限说明文本 + String _getExplanationText(Permission permission) { + switch (permission) { + case Permission.phone: + return '拨打电话功能需要电话权限'; + case Permission.sms: + return '发送短信功能需要短信权限'; + case Permission.calendarWriteOnly: + return '创建日历事件需要日历写入权限'; + case Permission.calendarFullAccess: + return '管理日历事件需要完整日历权限'; + case Permission.location: + return '获取位置信息需要位置权限'; + default: + return '应用需要此权限以提供完整功能'; } } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index ae68da38d..cae202efa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: audio_session: ^0.1.18 flutter_markdown: ^0.7.7+1 markdown: ^7.1.1 - permission_handler: ^11.4.0 + permission_handler: ^12.0.0+1 web_socket_channel: ^3.0.2 flutter_dotenv: ^5.1.0 synchronized: ^3.1.0