Browse Source

feat(权限): 完善权限请求流程并更新隐私政策文本

1. 在PermissionUtil中重构权限请求流程,增加解释横幅和更好的状态处理
2. 更新登录界面的隐私政策文本,明确用户选择后果
3. 将MusicManager和MeetingController的权限请求迁移到PermissionUtil统一处理
4. 增加权限请求失败时的详细日志记录和用户引导
weicu
lxm 1 year ago
parent
commit
eea7cfd57f
  1. 92
      lib/core/utils/permission_util.dart
  2. 31
      lib/data/services/music_manager.dart
  3. 8
      lib/modules/login/controllers/login_controller.dart
  4. 26
      lib/modules/meeting/controllers/meeting_controller.dart

92
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 'package:app_settings/app_settings.dart';
import 'logger.dart'; import 'logger.dart';
/// 权限请求工具类
///
/// 提供统一的权限请求流程处理:
/// 1. 初始权限状态检查
/// 2. 权限解释横幅显示(悬浮在UI顶部的解释性UI)
/// 3. 权限请求执行
/// 4. 永久拒绝后的设置引导
///
/// 使用单例模式:`PermissionUtil.instance`
class PermissionUtil { class PermissionUtil {
/// 单例实例
static final PermissionUtil instance = PermissionUtil._(); static final PermissionUtil instance = PermissionUtil._();
/// 私有构造函数
PermissionUtil._(); PermissionUtil._();
/// 权限解释横幅的叠加层入口
OverlayEntry? _explanationOverlayEntry; OverlayEntry? _explanationOverlayEntry;
/// 请求指定权限
///
/// 参数:
/// [context]: BuildContext,用于UI显示和导航
/// [permissionType]: 要请求的权限类型(Permission.microphone等)
/// [permissionName]: 权限的用户友好名称(如"麦克风"、"定位")
/// [explanationText]: 权限用途的详细解释文本
/// [permanentDenialText]: 永久拒绝后引导用户去设置的文案
/// [settingsButtonText]: "去设置"按钮的文本(可选,默认"前往设置")
/// [cancelButtonText]: "取消"按钮的文本(可选,默认"取消")
///
/// 返回值:`Future<bool>` 权限是否最终授予
Future<bool> requestPermission({ Future<bool> requestPermission({
required BuildContext context, required BuildContext context,
required Permission permissionType, required Permission permissionType,
@ -19,6 +45,7 @@ class PermissionUtil {
String cancelButtonText = '取消', String cancelButtonText = '取消',
}) async { }) async {
try { try {
// 检查初始权限状态
final PermissionStatus status = await permissionType.status; final PermissionStatus status = await permissionType.status;
Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status'); Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status');
@ -29,48 +56,66 @@ class PermissionUtil {
return true; return true;
} }
// 处理永久拒绝状态 // 显示权限解释横幅(覆盖所有状态的处理)
_showExplanationOverlay(
context: context,
permissionName: permissionName,
explanationText: explanationText,
);
// 处理永久拒绝状态(先显示说明横幅,再引导用户去设置)
if (status == PermissionStatus.permanentlyDenied) { if (status == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框'); Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框');
await _showPermanentDenialDialog(
// 显示永久拒绝对话框(在解释横幅下方显示)
final result = await _showPermanentDenialDialog(
context: context, context: context,
permissionName: permissionName, permissionName: permissionName,
permanentDenialText: permanentDenialText, permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText, settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText, cancelButtonText: cancelButtonText,
); );
return false; // 移除解释横幅
_hideExplanationOverlay();
return result;
} }
// 处理初始拒绝/未决定状态 // 处理初始拒绝/未决定状态
if (status == PermissionStatus.denied) { if (status == PermissionStatus.denied) {
Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求'); Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求');
_showExplanationOverlay( // _showExplanationOverlay(
context: context, // context: context,
permissionName: permissionName, // permissionName: permissionName,
explanationText: explanationText, // explanationText: explanationText,
); // );
//请求权限
final PermissionStatus newStatus = await permissionType.request(); final PermissionStatus newStatus = await permissionType.request();
Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus'); Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus');
_hideExplanationOverlay(); //_hideExplanationOverlay();
// 处理请求后的状态 // 处理请求后的状态
if (newStatus == PermissionStatus.granted || if (newStatus == PermissionStatus.granted ||
newStatus == PermissionStatus.provisional) { newStatus == PermissionStatus.provisional) {
Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予'); Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予');
_hideExplanationOverlay();
return true; return true;
} else if (newStatus == PermissionStatus.permanentlyDenied) { }
// 处理请求后变为永久拒绝
else if (newStatus == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝'); Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝');
await _showPermanentDenialDialog( final result = await _showPermanentDenialDialog(
context: context, context: context,
permissionName: permissionName, permissionName: permissionName,
permanentDenialText: permanentDenialText, permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText, settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText, cancelButtonText: cancelButtonText,
); );
_hideExplanationOverlay();
return result;
} }
// 其他拒绝状态
_hideExplanationOverlay();
return false; return false;
} }
@ -79,11 +124,13 @@ class PermissionUtil {
status == PermissionStatus.limited) { status == PermissionStatus.limited) {
Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status'); Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status');
await _showRestrictedPermissionDialog(context, permissionName); await _showRestrictedPermissionDialog(context, permissionName);
_hideExplanationOverlay();
return false; return false;
} }
// 其他未知状态处理 // 其他未知状态处理
Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status'); Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status');
_hideExplanationOverlay();
return false; return false;
} catch (e, stackTrace) { } catch (e, stackTrace) {
Logger.e( Logger.e(
@ -114,8 +161,9 @@ class PermissionUtil {
right: 16, right: 16,
child: Material( child: Material(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
elevation: 4, elevation: 8,
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
shadowColor: Colors.black.withOpacity(0.3),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
@ -161,7 +209,7 @@ class PermissionUtil {
/// 显示永久拒绝后的设置引导对话框 /// 显示永久拒绝后的设置引导对话框
/// ///
/// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限 /// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限
Future<void> _showPermanentDenialDialog({ Future<bool> _showPermanentDenialDialog({
required BuildContext context, required BuildContext context,
required String permissionName, required String permissionName,
required String permanentDenialText, required String permanentDenialText,
@ -170,7 +218,7 @@ class PermissionUtil {
}) async { }) async {
Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)'); Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)');
await showDialog<void>( bool? userChoice = await showDialog<bool>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@ -180,21 +228,29 @@ class PermissionUtil {
TextButton( TextButton(
onPressed: () { onPressed: () {
Logger.d('PermissionUtil', '用户取消设置 ($permissionName)'); Logger.d('PermissionUtil', '用户取消设置 ($permissionName)');
Navigator.pop(context); Navigator.pop(context, false);
}, },
child: Text(cancelButtonText), child: Text(cancelButtonText),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)'); Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)');
Navigator.pop(context); Navigator.pop(context, true);
AppSettings.openAppSettings();
}, },
child: Text(settingsButtonText), child: Text(settingsButtonText),
), ),
], ],
), ),
); );
// 用户选择前往设置
if (userChoice == true) {
AppSettings.openAppSettings();
return false; // 不能立即获取权限,需要用户手动设置
}
// 用户取消
return false;
} }
/// 显示受限权限对话框 /// 显示受限权限对话框

31
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:get/get.dart';
import 'package:music_service/music_service.dart'; import 'package:music_service/music_service.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import '../../core/utils/permission_util.dart';
import 'package:flutter/material.dart';
//音乐播放管理 //音乐播放管理
class MusicManager extends GetxService { class MusicManager extends GetxService {
@ -14,16 +16,35 @@ class MusicManager extends GetxService {
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
requestNotificationPermission();
final context = Get.context;
if (context == null) {
Logger.error('无法获取 BuildContext');
return;
}
requestNotificationPermission(context);
Logger.info('初始化权限请求'); Logger.info('初始化权限请求');
} }
Future<void> requestNotificationPermission() async { Future<void> requestNotificationPermission(BuildContext context) async {
if (await Permission.notification.isDenied) { final granted = await PermissionUtil.instance.requestPermission(
await Permission.notification.request(); context: context,
} permissionType: Permission.notification,
permissionName: '通知',
explanationText: '需要通知权限来显示音乐播放状态。',
permanentDenialText: '您已永久拒绝通知权限,请前往设置开启权限。',
settingsButtonText: '去设置',
cancelButtonText: '取消',
);
if (!granted) {
Logger.warning('通知权限未被授予');
return;
} }
Logger.info('通知权限已授予');
}
/// 释放资源 /// 释放资源
@override @override
void onClose() { void onClose() {

8
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, textAlign: TextAlign.center,
@ -293,10 +297,12 @@ class LoginController extends GetxController {
style: TextButton.styleFrom( style: TextButton.styleFrom(
foregroundColor: Colors.black54, foregroundColor: Colors.black54,
), ),
child: const Text('取消'), child: const Text('不同意'),
), ),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
_storage.write(
'hasAcceptedPrivacy', true); // 存储用户已接受隐私政策
// 用户同意,关闭弹窗并继续 // 用户同意,关闭弹窗并继续
_hasAcceptedPrivacy.value = true; _hasAcceptedPrivacy.value = true;
Get.back(); Get.back();

26
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/logger.dart';
import '../../../core/utils/upload_oss.dart'; import '../../../core/utils/upload_oss.dart';
import '../../../data/services/db/sqflite_api.dart'; import '../../../data/services/db/sqflite_api.dart';
import '../../../core/utils/permission_util.dart';
class MeetingController extends GetxController { class MeetingController extends GetxController {
final ScrollController listController = ScrollController(); final ScrollController listController = ScrollController();
@ -41,7 +42,8 @@ class MeetingController extends GetxController {
void onInit() { void onInit() {
super.onInit(); super.onInit();
listController.addListener(_onScroll); listController.addListener(_onScroll);
Permission.storage.request(); _requestStoragePermission();
//Permission.storage.request();
_headers = { _headers = {
'Authorization': 'Bearer; $_token', 'Authorization': 'Bearer; $_token',
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -83,6 +85,28 @@ class MeetingController extends GetxController {
isLoading = false; 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 { void addAudio() async {
EasyLoading.show(); EasyLoading.show();
FilePickerResult? result = await FilePicker.platform.pickFiles( FilePickerResult? result = await FilePicker.platform.pickFiles(

Loading…
Cancel
Save