Browse Source

refactor(权限管理): 重构权限处理逻辑并使用统一工具类

- 升级 permission_handler 依赖至 12.0.0+1
- 创建 PermissionUtil 工具类统一处理权限请求逻辑
- 在 AgentController 和 PairingController 中使用 PermissionUtil 替代原有权限检查方法
- 改进权限请求时的用户引导界面和提示信息
- 在权限设置页面集成新的权限请求流程
weicu
lxm 1 year ago
parent
commit
91b3f2320e
  1. 286
      lib/core/utils/permission_util.dart
  2. 21
      lib/modules/agent/controllers/agent_controller.dart
  3. 67
      lib/modules/pairing/controllers/pairing_controller.dart
  4. 171
      lib/modules/settings/views/permissions_view.dart
  5. 2
      pubspec.yaml

286
lib/core/utils/permission_util.dart

@ -1,134 +1,202 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.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 { class PermissionUtil {
/// 检查并请求多个权限 static final PermissionUtil instance = PermissionUtil._();
/// PermissionUtil._();
/// [permissions] 要检查的权限列表,如 [Permission.microphone, Permission.location]
/// [rationaleTitle] 弹窗标题 OverlayEntry? _explanationOverlayEntry;
/// [rationaleMessage] 权限用途说明内容(支持多行)
/// Future<bool> requestPermission({
/// 返回是否所有权限都已被授予 required BuildContext context,
static Future<bool> checkAndRequestPermissions({ required Permission permissionType,
required List<Permission> permissions, required String permissionName,
required String rationaleTitle, required String explanationText,
required List<String> rationaleMessage, required String permanentDenialText,
String settingsButtonText = '前往设置',
String cancelButtonText = '取消',
}) async { }) async {
try { try {
Map<Permission, PermissionStatus> statuses = {}; final PermissionStatus status = await permissionType.status;
log('初始权限状态 for $permissionName: $status');
// 检查是否都已授权
bool allGranted = true; // 处理已授权状态(包括授予状态和临时通知权限)
for (var permission in permissions) { if (status == PermissionStatus.granted ||
final status = await permission.status; status == PermissionStatus.provisional) {
if (!status.isGranted) { log('$permissionName 权限已授予或临时授权。');
allGranted = false; return true;
statuses[permission] = status; }
// 处理永久拒绝状态
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; // 处理受限状态
if (status == PermissionStatus.restricted ||
// 判断是否包含永久拒绝 status == PermissionStatus.limited) {
bool hasPermanentDenied = log('$permissionName 权限受限或受限访问: $status');
statuses.values.any((status) => status.isPermanentlyDenied); await _showRestrictedPermissionDialog(context, permissionName);
return false;
// 显示提示弹窗 }
_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");
}
}
},
);
// 其他未知状态处理
log('$permissionName 处于未知状态: $status');
return false; return false;
} catch (e) { } catch (e) {
print("权限检查异常: $e"); Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e');
_hideExplanationOverlay();
return false; return false;
} }
} }
/// 权限说明弹窗 void _showExplanationOverlay({
static void _showPermissionDialog({ required BuildContext context,
required bool isPermanent, required String permissionName,
required String title, required String explanationText,
required List<String> messages,
required VoidCallback onConfirm,
}) { }) {
Get.generalDialog( _hideExplanationOverlay(); // 确保移除旧叠加层
barrierDismissible: false,
barrierLabel: '权限提示', _explanationOverlayEntry = OverlayEntry(
transitionBuilder: (context, anim1, anim2, child) { builder: (context) => Positioned(
return SlideTransition( top: MediaQuery.of(context).padding.top + 10,
position: Tween<Offset>( left: 16,
begin: Offset(0, -1), right: 16,
end: Offset(0, 0), child: Material(
).animate(CurvedAnimation(parent: anim1, curve: Curves.easeOut)), borderRadius: BorderRadius.circular(12),
child: child, elevation: 4,
); color: Colors.white,
}, child: Padding(
pageBuilder: (context, anim1, anim2) { padding: const EdgeInsets.all(16),
return SafeArea( child: Column(
child: Align( mainAxisSize: MainAxisSize.min,
alignment: Alignment.topCenter, crossAxisAlignment: CrossAxisAlignment.start,
child: Padding( children: [
padding: const EdgeInsets.only(top: 80, left: 24, right: 24), Text(
child: Material( '需要$permissionName权限',
borderRadius: BorderRadius.circular(12), style: const TextStyle(
color: Colors.white, fontWeight: FontWeight.bold,
child: Padding( fontSize: 16,
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 ? '前往设置' : '授权'),
),
],
),
],
), ),
), ),
), 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<void> _showPermanentDenialDialog({
required BuildContext context,
required String permissionName,
required String permanentDenialText,
required String settingsButtonText,
required String cancelButtonText,
}) async {
await showDialog<void>(
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<void> _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('确定'),
),
],
),
); );
} }
} }

21
lib/modules/agent/controllers/agent_controller.dart

@ -698,7 +698,16 @@ class AgentController extends GetxController {
// 如果要切换到语音模式,需要先检查麦克风权限 // 如果要切换到语音模式,需要先检查麦克风权限
if (isTextInputMode.value) { 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) { if (!hasPermission) {
return; // 权限未获取,保持文本模式 return; // 权限未获取,保持文本模式
} }
@ -871,7 +880,15 @@ class AgentController extends GetxController {
if (isListening.value) return; 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) { if (!hasPermission) {
isTextInputMode.value = true; // 如果没有权限,切回文本模式 isTextInputMode.value = true; // 如果没有权限,切回文本模式
return; return;

67
lib/modules/pairing/controllers/pairing_controller.dart

@ -7,6 +7,7 @@ import '../../../core/utils/logger.dart';
import 'dart:io'; import 'dart:io';
import '../../../data/services/bluetooth_manager.dart'; import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/ble_manager.dart'; import '../../../data/services/ble_manager.dart';
import '../../../core/utils/permission_util.dart';
/// 配对状态枚举 /// 配对状态枚举
enum PairingState { enum PairingState {
@ -508,23 +509,75 @@ class PairingController extends GetxController {
return true; return true;
} }
// Android权限检查 // Android权限检查 - 使用 PermissionUtil
final permissions = <Permission>[]; final context = Get.context;
permissions.add(Permission.locationWhenInUse); if (context == null) {
Logger.e(_tag, '无法获取有效上下文,跳过权限请求');
return false;
}
// 根据 Android 版本确定需要的权限
List<Permission> requiredPermissions = [
Permission.locationWhenInUse,
];
if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) { if (Platform.isAndroid && await _getAndroidSdkVersion() >= 31) {
permissions.add(Permission.bluetoothScan); requiredPermissions.add(Permission.bluetoothScan);
permissions.add(Permission.bluetoothConnect); 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) { } catch (e) {
Logger.e(_tag, '权限检查失败: $e'); Logger.e(_tag, '权限检查失败: $e');
return false; 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() { void _setupBluetoothListener() {
_bluetoothStateSubscription?.cancel(); _bluetoothStateSubscription?.cancel();

171
lib/modules/settings/views/permissions_view.dart

@ -4,6 +4,7 @@ import 'package:permission_handler/permission_handler.dart';
import '../controllers/settings_controller.dart'; import '../controllers/settings_controller.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'dart:ui'; import 'dart:ui';
import 'package:deep_voice/core/utils/permission_util.dart'; // 引入PermissionUtil
class PermissionsView extends GetView<SettingsController> { class PermissionsView extends GetView<SettingsController> {
const PermissionsView({Key? key}) : super(key: key); const PermissionsView({Key? key}) : super(key: key);
@ -52,24 +53,28 @@ class PermissionsView extends GetView<SettingsController> {
padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h), padding: EdgeInsets.symmetric(horizontal: 16.w, vertical: 16.h),
children: [ children: [
// 通讯权限组 // 通讯权限组
_buildPermissionSection('通讯权限', Icons.phone_android, [ _buildPermissionSection(
Permission.phone, '通讯权限',
Permission.sms Icons.phone_android,
], permissionStatuses, isDarkMode), [Permission.phone, Permission.sms],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
// 日历权限组 // 日历权限组
_buildPermissionSection('日历权限', Icons.calendar_today, [ _buildPermissionSection(
Permission.calendarFullAccess '日历权限',
], permissionStatuses, isDarkMode), Icons.calendar_today,
[Permission.calendarFullAccess],
permissionStatuses,
isDarkMode),
SizedBox(height: 20.h), SizedBox(height: 20.h),
// 位置权限组 // 位置权限组
_buildPermissionSection('位置权限', Icons.location_on, [ _buildPermissionSection('位置权限', Icons.location_on,
Permission.location [Permission.location], permissionStatuses, isDarkMode),
], permissionStatuses, isDarkMode),
], ],
); );
}, },
@ -78,8 +83,12 @@ class PermissionsView extends GetView<SettingsController> {
} }
// 构建权限组区域 // 构建权限组区域
Widget _buildPermissionSection(String title, IconData icon, List<Permission> permissions, Widget _buildPermissionSection(
Map<Permission, PermissionStatus> statuses, bool isDarkMode) { String title,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -119,7 +128,8 @@ class PermissionsView extends GetView<SettingsController> {
? ImageFilter.blur(sigmaX: 5, sigmaY: 5) ? ImageFilter.blur(sigmaX: 5, sigmaY: 5)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0), : ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Column( child: Column(
children: _buildPermissionItems(permissions, statuses, isDarkMode), children:
_buildPermissionItems(permissions, statuses, isDarkMode),
), ),
), ),
), ),
@ -133,39 +143,39 @@ class PermissionsView extends GetView<SettingsController> {
Map<Permission, PermissionStatus> statuses, bool isDarkMode) { Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> items = []; final List<Widget> items = [];
// 获取描述文本 // 获取名称
String getDescription(Permission permission) { String getName(Permission permission) {
switch (permission) { switch (permission) {
case Permission.phone: case Permission.phone:
return '用于拨打电话功能'; return '电话';
case Permission.sms: case Permission.sms:
return '用于发送短信功能'; return '短信';
case Permission.calendarWriteOnly: case Permission.calendarWriteOnly:
return '用于创建日历事件和提醒'; return '日历写入';
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return '用于读取和管理日历事件及提醒'; return '日历完整访问';
case Permission.location: case Permission.location:
return '用于获取当前位置信息'; return '位置信息';
default: default:
return '应用功能需要'; return permission.toString();
} }
} }
// 获取名称 // 获取描述文本
String getName(Permission permission) { String getDescription(Permission permission) {
switch (permission) { switch (permission) {
case Permission.phone: case Permission.phone:
return '电话'; return '用于拨打电话功能';
case Permission.sms: case Permission.sms:
return '短信'; return '用于发送短信功能';
case Permission.calendarWriteOnly: case Permission.calendarWriteOnly:
return '日历写入'; return '用于创建日历事件和提醒';
case Permission.calendarFullAccess: case Permission.calendarFullAccess:
return '日历完整访问'; return '用于读取和管理日历事件及提醒';
case Permission.location: case Permission.location:
return '位置信息'; return '用于获取当前位置信息';
default: default:
return permission.toString(); return '应用功能需要';
} }
} }
@ -262,7 +272,7 @@ class PermissionsView extends GetView<SettingsController> {
color = Colors.red; color = Colors.red;
} else if (status.isPermanentlyDenied) { } else if (status.isPermanentlyDenied) {
text = '需要在系统设置中开启'.tr; text = '需要在系统设置中开启'.tr;
color = Colors.grey; color = Colors.orange;
} else if (status.isLimited) { } else if (status.isLimited) {
text = '部分授权'.tr; text = '部分授权'.tr;
color = Colors.orange; color = Colors.orange;
@ -282,43 +292,82 @@ class PermissionsView extends GetView<SettingsController> {
} }
// 处理权限请求 // 处理权限请求
Future<void> _handlePermissionAction(Permission permission, PermissionStatus status) async { Future<void> _handlePermissionAction(
if (status.isPermanentlyDenied) { Permission permission, PermissionStatus status) async {
// 打开应用设置 try {
controller.openSettings(); final context = Get.context;
return; if (context == null) return;
}
if (!status.isGranted) { final permissionName = _getPermissionName(permission);
// 请求权限 final explanationText = _getExplanationText(permission);
final result = await controller.requestPermission(permission);
if (result.isGranted) { // 使用 PermissionUtil 请求权限
Get.snackbar('成功'.tr, '权限已授予'.tr, snackPosition: SnackPosition.BOTTOM); final hasPermission = await PermissionUtil.instance.requestPermission(
} else if (result.isPermanentlyDenied) { context: context,
Get.dialog( permissionType: permission,
AlertDialog( permissionName: permissionName,
title: Text('需要权限'.tr), explanationText: explanationText,
content: Text('此权限需要在设置中手动开启'.tr), permanentDenialText: '您已永久拒绝$permissionName权限,请前往应用设置开启',
actions: [ settingsButtonText: '去设置',
TextButton( cancelButtonText: '取消',
onPressed: () => Get.back(), );
child: Text('取消'.tr),
), if (hasPermission) {
TextButton( Get.snackbar(
onPressed: () { '成功'.tr,
Get.back(); '$permissionName权限已授予'.tr,
controller.openSettings(); snackPosition: SnackPosition.BOTTOM,
}, backgroundColor: Colors.green,
child: Text('去设置'.tr), colorText: Colors.white,
),
],
),
); );
} }
// 刷新界面 // 刷新界面,重新获取权限状态
Get.forceAppUpdate(); 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 '应用需要此权限以提供完整功能';
} }
} }
} }

2
pubspec.yaml

@ -21,7 +21,7 @@ dependencies:
audio_session: ^0.1.18 audio_session: ^0.1.18
flutter_markdown: ^0.7.7+1 flutter_markdown: ^0.7.7+1
markdown: ^7.1.1 markdown: ^7.1.1
permission_handler: ^11.4.0 permission_handler: ^12.0.0+1
web_socket_channel: ^3.0.2 web_socket_channel: ^3.0.2
flutter_dotenv: ^5.1.0 flutter_dotenv: ^5.1.0
synchronized: ^3.1.0 synchronized: ^3.1.0

Loading…
Cancel
Save