From eea7cfd57f460ca2eae272f5335bc1b3239b6676 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 9 Jun 2025 15:56:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=9D=83=E9=99=90):=20=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82=E6=B5=81=E7=A8=8B=E5=B9=B6?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=9A=90=E7=A7=81=E6=94=BF=E7=AD=96=E6=96=87?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 在PermissionUtil中重构权限请求流程,增加解释横幅和更好的状态处理 2. 更新登录界面的隐私政策文本,明确用户选择后果 3. 将MusicManager和MeetingController的权限请求迁移到PermissionUtil统一处理 4. 增加权限请求失败时的详细日志记录和用户引导 --- lib/core/utils/permission_util.dart | 92 +++++++++++++++---- lib/data/services/music_manager.dart | 31 ++++++- .../login/controllers/login_controller.dart | 8 +- .../controllers/meeting_controller.dart | 26 +++++- 4 files changed, 132 insertions(+), 25 deletions(-) diff --git a/lib/core/utils/permission_util.dart b/lib/core/utils/permission_util.dart index 83f152ee5..0961e7120 100644 --- a/lib/core/utils/permission_util.dart +++ b/lib/core/utils/permission_util.dart @@ -3,12 +3,38 @@ import 'package:permission_handler/permission_handler.dart'; import 'package:app_settings/app_settings.dart'; import 'logger.dart'; +/// 权限请求工具类 +/// +/// 提供统一的权限请求流程处理: +/// 1. 初始权限状态检查 +/// 2. 权限解释横幅显示(悬浮在UI顶部的解释性UI) +/// 3. 权限请求执行 +/// 4. 永久拒绝后的设置引导 +/// +/// 使用单例模式:`PermissionUtil.instance` + class PermissionUtil { + /// 单例实例 static final PermissionUtil instance = PermissionUtil._(); + + /// 私有构造函数 PermissionUtil._(); + /// 权限解释横幅的叠加层入口 OverlayEntry? _explanationOverlayEntry; + /// 请求指定权限 + /// + /// 参数: + /// [context]: BuildContext,用于UI显示和导航 + /// [permissionType]: 要请求的权限类型(Permission.microphone等) + /// [permissionName]: 权限的用户友好名称(如"麦克风"、"定位") + /// [explanationText]: 权限用途的详细解释文本 + /// [permanentDenialText]: 永久拒绝后引导用户去设置的文案 + /// [settingsButtonText]: "去设置"按钮的文本(可选,默认"前往设置") + /// [cancelButtonText]: "取消"按钮的文本(可选,默认"取消") + /// + /// 返回值:`Future` 权限是否最终授予 Future requestPermission({ required BuildContext context, required Permission permissionType, @@ -19,6 +45,7 @@ class PermissionUtil { String cancelButtonText = '取消', }) async { try { + // 检查初始权限状态 final PermissionStatus status = await permissionType.status; Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status'); @@ -29,48 +56,66 @@ class PermissionUtil { return true; } - // 处理永久拒绝状态 + // 显示权限解释横幅(覆盖所有状态的处理) + _showExplanationOverlay( + context: context, + permissionName: permissionName, + explanationText: explanationText, + ); + + // 处理永久拒绝状态(先显示说明横幅,再引导用户去设置) if (status == PermissionStatus.permanentlyDenied) { Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框'); - await _showPermanentDenialDialog( + + // 显示永久拒绝对话框(在解释横幅下方显示) + final result = await _showPermanentDenialDialog( context: context, permissionName: permissionName, permanentDenialText: permanentDenialText, settingsButtonText: settingsButtonText, cancelButtonText: cancelButtonText, ); - return false; + // 移除解释横幅 + _hideExplanationOverlay(); + return result; } // 处理初始拒绝/未决定状态 if (status == PermissionStatus.denied) { Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求'); - _showExplanationOverlay( - context: context, - permissionName: permissionName, - explanationText: explanationText, - ); - + // _showExplanationOverlay( + // context: context, + // permissionName: permissionName, + // explanationText: explanationText, + // ); +//请求权限 final PermissionStatus newStatus = await permissionType.request(); Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus'); - _hideExplanationOverlay(); + //_hideExplanationOverlay(); // 处理请求后的状态 if (newStatus == PermissionStatus.granted || newStatus == PermissionStatus.provisional) { Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予'); + _hideExplanationOverlay(); return true; - } else if (newStatus == PermissionStatus.permanentlyDenied) { + } + // 处理请求后变为永久拒绝 + else if (newStatus == PermissionStatus.permanentlyDenied) { Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝'); - await _showPermanentDenialDialog( + final result = await _showPermanentDenialDialog( context: context, permissionName: permissionName, permanentDenialText: permanentDenialText, settingsButtonText: settingsButtonText, cancelButtonText: cancelButtonText, ); + _hideExplanationOverlay(); + return result; } + // 其他拒绝状态 + _hideExplanationOverlay(); return false; } @@ -79,11 +124,13 @@ class PermissionUtil { status == PermissionStatus.limited) { Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status'); await _showRestrictedPermissionDialog(context, permissionName); + _hideExplanationOverlay(); return false; } // 其他未知状态处理 Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status'); + _hideExplanationOverlay(); return false; } catch (e, stackTrace) { Logger.e( @@ -114,8 +161,9 @@ class PermissionUtil { right: 16, child: Material( borderRadius: BorderRadius.circular(12), - elevation: 4, + elevation: 8, color: Theme.of(context).colorScheme.background, + shadowColor: Colors.black.withOpacity(0.3), child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -161,7 +209,7 @@ class PermissionUtil { /// 显示永久拒绝后的设置引导对话框 /// /// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限 - Future _showPermanentDenialDialog({ + Future _showPermanentDenialDialog({ required BuildContext context, required String permissionName, required String permanentDenialText, @@ -170,7 +218,7 @@ class PermissionUtil { }) async { Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)'); - await showDialog( + bool? userChoice = await showDialog( context: context, barrierDismissible: false, builder: (context) => AlertDialog( @@ -180,21 +228,29 @@ class PermissionUtil { TextButton( onPressed: () { Logger.d('PermissionUtil', '用户取消设置 ($permissionName)'); - Navigator.pop(context); + Navigator.pop(context, false); }, child: Text(cancelButtonText), ), ElevatedButton( onPressed: () { Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)'); - Navigator.pop(context); - AppSettings.openAppSettings(); + Navigator.pop(context, true); }, child: Text(settingsButtonText), ), ], ), ); + + // 用户选择前往设置 + if (userChoice == true) { + AppSettings.openAppSettings(); + return false; // 不能立即获取权限,需要用户手动设置 + } + + // 用户取消 + return false; } /// 显示受限权限对话框 diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index af7e91481..7e19942a6 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -2,6 +2,8 @@ import 'package:deep_voice/core/utils/logger.dart'; import 'package:get/get.dart'; import 'package:music_service/music_service.dart'; import 'package:permission_handler/permission_handler.dart'; +import '../../core/utils/permission_util.dart'; +import 'package:flutter/material.dart'; //音乐播放管理 class MusicManager extends GetxService { @@ -14,16 +16,35 @@ class MusicManager extends GetxService { @override void onInit() { super.onInit(); - requestNotificationPermission(); + + final context = Get.context; + if (context == null) { + Logger.error('无法获取 BuildContext'); + return; + } + requestNotificationPermission(context); Logger.info('初始化权限请求'); } - Future requestNotificationPermission() async { - if (await Permission.notification.isDenied) { - await Permission.notification.request(); - } + Future requestNotificationPermission(BuildContext context) async { + final granted = await PermissionUtil.instance.requestPermission( + context: context, + permissionType: Permission.notification, + permissionName: '通知', + explanationText: '需要通知权限来显示音乐播放状态。', + permanentDenialText: '您已永久拒绝通知权限,请前往设置开启权限。', + settingsButtonText: '去设置', + cancelButtonText: '取消', + ); + + if (!granted) { + Logger.warning('通知权限未被授予'); + return; } + Logger.info('通知权限已授予'); +} + /// 释放资源 @override void onClose() { diff --git a/lib/modules/login/controllers/login_controller.dart b/lib/modules/login/controllers/login_controller.dart index 1ac7e8af7..82c246def 100644 --- a/lib/modules/login/controllers/login_controller.dart +++ b/lib/modules/login/controllers/login_controller.dart @@ -276,6 +276,10 @@ class LoginController extends GetxController { ), ), ), + const TextSpan( + text: ',点击“同意并进入”表示您已充分阅读并同意上述协议。点击“不同意”将退出应用。', + style: TextStyle(fontSize: 14), + ), ], ), textAlign: TextAlign.center, @@ -293,10 +297,12 @@ class LoginController extends GetxController { style: TextButton.styleFrom( foregroundColor: Colors.black54, ), - child: const Text('取消'), + child: const Text('不同意'), ), ElevatedButton( onPressed: () { + _storage.write( + 'hasAcceptedPrivacy', true); // 存储用户已接受隐私政策 // 用户同意,关闭弹窗并继续 _hasAcceptedPrivacy.value = true; Get.back(); diff --git a/lib/modules/meeting/controllers/meeting_controller.dart b/lib/modules/meeting/controllers/meeting_controller.dart index 8322dc380..b191326b7 100644 --- a/lib/modules/meeting/controllers/meeting_controller.dart +++ b/lib/modules/meeting/controllers/meeting_controller.dart @@ -13,6 +13,7 @@ import 'package:permission_handler/permission_handler.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/upload_oss.dart'; import '../../../data/services/db/sqflite_api.dart'; +import '../../../core/utils/permission_util.dart'; class MeetingController extends GetxController { final ScrollController listController = ScrollController(); @@ -41,7 +42,8 @@ class MeetingController extends GetxController { void onInit() { super.onInit(); listController.addListener(_onScroll); - Permission.storage.request(); + _requestStoragePermission(); + //Permission.storage.request(); _headers = { 'Authorization': 'Bearer; $_token', 'Content-Type': 'application/json', @@ -83,6 +85,28 @@ class MeetingController extends GetxController { isLoading = false; } + // 请求录音权限 + void _requestStoragePermission() async { + final BuildContext context = Get.overlayContext!; + + PermissionUtil.instance + .requestPermission( + context: context, + permissionType: Permission.storage, + permissionName: "存储", + explanationText: "我们需要访问存储权限,以便您可以选择并上传音频文件。", + permanentDenialText: "检测到您已永久拒绝存储权限。如需继续使用此功能,请前往应用设置中手动开启存储权限。", + ) + .then((granted) { + if (granted) { + Logger.d("Permission", "用户授予了存储权限"); + } else { + Logger.d("Permission", "用户未授予存储权限"); + // 可以提示用户无法进行文件操作 + } + }); + } + void addAudio() async { EasyLoading.show(); FilePickerResult? result = await FilePicker.platform.pickFiles(