From 322ce5a74c6a7ac875f419920fe0d58f6c1bbc11 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Sat, 7 Jun 2025 20:14:31 +0800 Subject: [PATCH 1/3] =?UTF-8?q?refactor(permission):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E6=9D=83=E9=99=90=E5=B7=A5=E5=85=B7=E7=B1=BB=E4=BD=BF=E7=94=A8?= =?UTF-8?q?Logger=E6=9B=BF=E4=BB=A3=E7=9B=B4=E6=8E=A5=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/utils/permission_util.dart | 69 +++++++++++++++++++---------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/lib/core/utils/permission_util.dart b/lib/core/utils/permission_util.dart index e51aafb3f..83f152ee5 100644 --- a/lib/core/utils/permission_util.dart +++ b/lib/core/utils/permission_util.dart @@ -1,15 +1,8 @@ import 'package:flutter/material.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 { static final PermissionUtil instance = PermissionUtil._(); PermissionUtil._(); @@ -27,18 +20,18 @@ class PermissionUtil { }) async { try { final PermissionStatus status = await permissionType.status; - log('初始权限状态 for $permissionName: $status'); + Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status'); // 处理已授权状态(包括授予状态和临时通知权限) if (status == PermissionStatus.granted || status == PermissionStatus.provisional) { - log('$permissionName 权限已授予或临时授权。'); + Logger.i('PermissionUtil', '$permissionName 权限已授予或临时授权'); return true; } // 处理永久拒绝状态 if (status == PermissionStatus.permanentlyDenied) { - log('$permissionName 权限被永久拒绝,显示引导对话框。'); + Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框'); await _showPermanentDenialDialog( context: context, permissionName: permissionName, @@ -51,7 +44,7 @@ class PermissionUtil { // 处理初始拒绝/未决定状态 if (status == PermissionStatus.denied) { - log('$permissionName 权限被拒绝或未决定,显示说明横幅并请求。'); + Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求'); _showExplanationOverlay( context: context, permissionName: permissionName, @@ -59,17 +52,17 @@ class PermissionUtil { ); final PermissionStatus newStatus = await permissionType.request(); - log('请求后 $permissionName 的新权限状态: $newStatus'); + Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus'); _hideExplanationOverlay(); // 处理请求后的状态 if (newStatus == PermissionStatus.granted || newStatus == PermissionStatus.provisional) { - log('$permissionName 权限在请求后被授予或临时授权。'); + Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予'); return true; } else if (newStatus == PermissionStatus.permanentlyDenied) { - log('$permissionName 权限在请求后被永久拒绝,再次显示引导对话框。'); + Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝'); await _showPermanentDenialDialog( context: context, permissionName: permissionName, @@ -84,21 +77,29 @@ class PermissionUtil { // 处理受限状态 if (status == PermissionStatus.restricted || status == PermissionStatus.limited) { - log('$permissionName 权限受限或受限访问: $status'); + Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status'); await _showRestrictedPermissionDialog(context, permissionName); return false; } // 其他未知状态处理 - log('$permissionName 处于未知状态: $status'); + Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status'); return false; - } catch (e) { - Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e'); + } catch (e, stackTrace) { + Logger.e( + 'PermissionUtil', + '请求 $permissionName 权限失败: $e', + e, + stackTrace, + ); _hideExplanationOverlay(); return false; } } + /// 显示权限解释横幅(漂浮在UI顶部的说明性UI) + /// + /// 在请求权限前显示,向用户解释为什么需要这个权限 void _showExplanationOverlay({ required BuildContext context, required String permissionName, @@ -114,7 +115,7 @@ class PermissionUtil { child: Material( borderRadius: BorderRadius.circular(12), elevation: 4, - color: Colors.white, + color: Theme.of(context).colorScheme.background, child: Padding( padding: const EdgeInsets.all(16), child: Column( @@ -143,16 +144,23 @@ class PermissionUtil { Future.delayed(Duration.zero, () { if (_explanationOverlayEntry != null) { Overlay.of(context).insert(_explanationOverlayEntry!); - log('权限说明叠加层已显示。'); + Logger.i('PermissionUtil', '权限说明叠加层已显示 ($permissionName)'); } }); } + /// 隐藏权限解释横幅 void _hideExplanationOverlay() { - _explanationOverlayEntry?.remove(); - _explanationOverlayEntry = null; + if (_explanationOverlayEntry != null) { + _explanationOverlayEntry?.remove(); + _explanationOverlayEntry = null; + Logger.d('PermissionUtil', '权限说明叠加层已移除'); + } } + /// 显示永久拒绝后的设置引导对话框 + /// + /// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限 Future _showPermanentDenialDialog({ required BuildContext context, required String permissionName, @@ -160,6 +168,8 @@ class PermissionUtil { required String settingsButtonText, required String cancelButtonText, }) async { + Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)'); + await showDialog( context: context, barrierDismissible: false, @@ -168,11 +178,15 @@ class PermissionUtil { content: Text(permanentDenialText), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + Logger.d('PermissionUtil', '用户取消设置 ($permissionName)'); + Navigator.pop(context); + }, child: Text(cancelButtonText), ), ElevatedButton( onPressed: () { + Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)'); Navigator.pop(context); AppSettings.openAppSettings(); }, @@ -183,8 +197,12 @@ class PermissionUtil { ); } + /// 显示受限权限对话框 + /// + /// 在权限受限状态(如iOS上的受限照片访问)时调用 Future _showRestrictedPermissionDialog( BuildContext context, String permissionName) async { + Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)'); await showDialog( context: context, builder: (context) => AlertDialog( @@ -192,7 +210,10 @@ class PermissionUtil { content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'), actions: [ TextButton( - onPressed: () => Navigator.pop(context), + onPressed: () { + Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)'); + Navigator.pop(context); + }, child: const Text('确定'), ), ], From b76722dcd7d26d01945cceb8d2e978c224a29da4 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 9 Jun 2025 15:56:49 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(=E6=9D=83=E9=99=90):=20=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E9=9A=90=E7=A7=81=E6=94=BF=E7=AD=96?= =?UTF-8?q?=E6=96=87=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( From 600edf13da7bcedf73623d781a1a0d0b8dd154e3 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Mon, 9 Jun 2025 20:45:46 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(AndroidManifest):=20=E7=A7=BB=E9=99=A4M?= =?UTF-8?q?ANAGE=5FEXTERNAL=5FSTORAGE=E6=9D=83=E9=99=90=E5=92=8CrequestLeg?= =?UTF-8?q?acyExternalStorage=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移除不再需要的MANAGE_EXTERNAL_STORAGE权限声明,并删除requestLegacyExternalStorage属性以适配Android存储策略变更。这些权限和属性在最新Android版本中已不再适用或推荐使用。 --- android/app/src/main/AndroidManifest.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 6a87534b7..9b63b731d 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -26,8 +26,8 @@ - + @@ -58,7 +58,7 @@ android:label="DeapSound" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" - android:requestLegacyExternalStorage="true" + android:networkSecurityConfig="@xml/network_security_config" android:usesCleartextTraffic="true" android:enableOnBackInvokedCallback="true"