Browse Source

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

- 升级 permission_handler 依赖至 12.0.0+1
- 创建 PermissionUtil 工具类统一处理权限请求逻辑
- 在 AgentController 和 PairingController 中使用 PermissionUtil 替代原有权限检查方法
- 改进权限请求时的用户引导界面和提示信息
- 在权限设置页面集成新的权限请求流程
newdev_shunjiawei
lxm 1 year ago
parent
commit
33bbb4ba7f
  1. 286
      lib/core/utils/permission_util.dart
  2. 21
      lib/modules/agent/controllers/agent_controller.dart
  3. 71
      lib/modules/pairing/controllers/pairing_controller.dart
  4. 225
      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: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<bool> checkAndRequestPermissions({
required List<Permission> permissions,
required String rationaleTitle,
required List<String> rationaleMessage,
static final PermissionUtil instance = PermissionUtil._();
PermissionUtil._();
OverlayEntry? _explanationOverlayEntry;
Future<bool> requestPermission({
required BuildContext context,
required Permission permissionType,
required String permissionName,
required String explanationText,
required String permanentDenialText,
String settingsButtonText = '前往设置',
String cancelButtonText = '取消',
}) async {
try {
Map<Permission, PermissionStatus> 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<String> 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<Offset>(
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<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) {
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;

71
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 = <Permission>[];
permissions.add(Permission.locationWhenInUse);
// Android权限检查 - 使用 PermissionUtil
final context = Get.context;
if (context == null) {
Logger.e(_tag, '无法获取有效上下文,跳过权限请求');
return false;
}
// 根据 Android 版本确定需要的权限
List<Permission> 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();

225
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<SettingsController> {
const PermissionsView({Key? key}) : super(key: key);
@ -11,7 +12,7 @@ class PermissionsView extends GetView<SettingsController> {
@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<SettingsController> {
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<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
Widget _buildPermissionSection(
String title,
IconData icon,
List<Permission> permissions,
Map<Permission, PermissionStatus> statuses,
bool isDarkMode) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -96,7 +105,7 @@ class PermissionsView extends GetView<SettingsController> {
),
),
),
// 权限项容器
Container(
decoration: BoxDecoration(
@ -119,7 +128,8 @@ class PermissionsView extends GetView<SettingsController> {
? 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<SettingsController> {
],
);
}
// 构建权限项列表
List<Widget> _buildPermissionItems(List<Permission> permissions,
List<Widget> _buildPermissionItems(List<Permission> permissions,
Map<Permission, PermissionStatus> statuses, bool isDarkMode) {
final List<Widget> 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<SettingsController> {
isDarkMode: isDarkMode,
),
);
// 添加分隔线,最后一个不加
if (i < permissions.length - 1) {
items.add(Divider(
@ -191,10 +201,10 @@ class PermissionsView extends GetView<SettingsController> {
));
}
}
return items;
}
// 构建单个权限项
Widget _buildPermissionItem({
required String name,
@ -248,12 +258,12 @@ class PermissionsView extends GetView<SettingsController> {
),
);
}
// 状态文本
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<SettingsController> {
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<SettingsController> {
text = '未知状态'.tr;
color = Colors.grey;
}
return Text(
text,
style: TextStyle(
@ -280,45 +290,84 @@ class PermissionsView extends GetView<SettingsController> {
),
);
}
// 处理权限请求
Future<void> _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<void> _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 '应用需要此权限以提供完整功能';
}
}
}
}

2
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

Loading…
Cancel
Save