You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

345 lines
12 KiB

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:app_settings/app_settings.dart';
import 'logger.dart';
import 'package:get/get.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<bool>` 权限是否最终授予
Future<bool> requestPermission({
// required BuildContext context,
required Permission permissionType,
required String permissionName,
required String explanationText,
required String permanentDenialText,
String? settingsButtonText,
String? cancelButtonText,
}) async {
// 使用翻译键作为默认值
final String finalSettingsButtonText =
settingsButtonText ?? 'goToSettings'.tr;
final String finalCancelButtonText = cancelButtonText ?? 'cancel'.tr;
try {
// 检查初始权限状态
final PermissionStatus status = await permissionType.status;
Logger.d('PermissionUtil', '初始权限状态 [$permissionName]: $status');
// 处理已授权状态(包括授予状态和临时通知权限)
if (status == PermissionStatus.granted ||
status == PermissionStatus.provisional) {
Logger.i('PermissionUtil', '$permissionName 权限已授予或临时授权');
return true;
}
// 显示权限解释横幅(覆盖所有状态的处理)
_showExplanationOverlay(
//context: context,
permissionName: permissionName,
explanationText: explanationText,
);
// 处理永久拒绝状态(先显示说明横幅,再引导用户去设置)
if (status == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框');
// 显示永久拒绝对话框(在解释横幅下方显示)
final result = await _showPermanentDenialDialog(
// context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
// 移除解释横幅
_hideExplanationOverlay();
return result;
}
// 处理初始拒绝/未决定状态
if (status == PermissionStatus.denied) {
Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求');
// _showExplanationOverlay(
// context: context,
// permissionName: permissionName,
// explanationText: explanationText,
// );
//请求权限
final PermissionStatus newStatus = await permissionType.request();
Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus');
//_hideExplanationOverlay();
// 处理请求后的状态
if (newStatus == PermissionStatus.granted ||
newStatus == PermissionStatus.provisional) {
Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予');
_hideExplanationOverlay();
return true;
}
// 处理请求后变为永久拒绝
else if (newStatus == PermissionStatus.permanentlyDenied) {
Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝');
final result = await _showPermanentDenialDialog(
// context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: finalSettingsButtonText,
cancelButtonText: finalCancelButtonText,
);
_hideExplanationOverlay();
return result;
}
// 其他拒绝状态
_hideExplanationOverlay();
return false;
}
// 处理受限状态
if (status == PermissionStatus.restricted ||
status == PermissionStatus.limited) {
Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status');
await _showRestrictedPermissionDialog(permissionName);
_hideExplanationOverlay();
return false;
}
// 其他未知状态处理
Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status');
_hideExplanationOverlay();
return false;
} catch (e, stackTrace) {
Logger.e(
'PermissionUtil',
'请求 $permissionName 权限失败: $e',
e,
stackTrace,
);
_hideExplanationOverlay();
return false;
}
}
/// 显示权限解释横幅(漂浮在UI顶部的说明性UI)
///
/// 在请求权限前显示,向用户解释为什么需要这个权限
void _showExplanationOverlay({
required String permissionName,
required String explanationText,
}) {
Get.dialog(
Stack(
children: [
Positioned(
top: 10,
left: 16,
right: 16,
child: Material(
borderRadius: BorderRadius.circular(12),
elevation: 8,
color: Colors.grey[300],
shadowColor: Colors.black.withOpacity(0.3),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'permissionUtilNeed'.tr +
permissionName +
'permissionUtilPermission'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 8),
Text(
explanationText,
style: const TextStyle(fontSize: 14),
),
],
),
),
),
),
],
),
);
// _hideExplanationOverlay(); // 确保移除旧叠加层
// _explanationOverlayEntry = OverlayEntry(
// builder: (context) => Positioned(
// top: MediaQuery.of(context).padding.top + 10,
// left: 16,
// right: 16,
// child: Material(
// borderRadius: BorderRadius.circular(12),
// elevation: 8,
// color: Theme.of(context).colorScheme.background,
// shadowColor: Colors.black.withOpacity(0.3),
// 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!);
// Logger.i('PermissionUtil', '权限说明叠加层已显示 ($permissionName)');
// }
// });
}
/// 隐藏权限解释横幅
void _hideExplanationOverlay() {
Get.back();
//_explanationOverlayEntry!.remove();
Logger.d('PermissionUtil', '权限说明叠加层已移除');
}
/// 显示永久拒绝后的设置引导对话框
///
/// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限
Future<bool> _showPermanentDenialDialog({
//required BuildContext context,
required String permissionName,
required String permanentDenialText,
required String settingsButtonText,
required String cancelButtonText,
}) async {
Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)');
bool? userChoice = await Get.dialog(
AlertDialog(
title: Text('permissionUtilNeed'.tr +
permissionName +
'permissionUtilPermission'.tr),
content: Text(permanentDenialText),
actions: [
TextButton(
onPressed: () {
Logger.d('PermissionUtil', '用户取消设置 ($permissionName)');
Get.back(result: false);
},
child: Text(cancelButtonText),
),
ElevatedButton(
onPressed: () {
Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)');
Get.back(result: true);
},
child: Text(settingsButtonText),
),
],
),
barrierDismissible: false,
);
// bool? userChoice = await showDialog<bool>(
// context: context,
// barrierDismissible: false,
// builder: (context) =>
// );
// 用户选择前往设置
if (userChoice == true) {
AppSettings.openAppSettings();
return false; // 不能立即获取权限,需要用户手动设置
}
// 用户取消
return false;
}
/// 显示受限权限对话框
///
/// 在权限受限状态(如iOS上的受限照片访问)时调用
Future<void> _showRestrictedPermissionDialog(String permissionName) async {
Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)');
Get.dialog(
AlertDialog(
title: Text(permissionName + 'permissionUtilRestricted'.tr),
content: Text('permissionUtilRestrictedMessage'.tr),
actions: [
TextButton(
onPressed: () {
Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)');
// Navigator.pop(context);
Get.back();
},
child: Text('permissionUtilConfirm'.tr),
),
],
),
);
// await showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: Text('$permissionName权限受限'),
// content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'),
// actions: [
// TextButton(
// onPressed: () {
// Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)');
// Navigator.pop(context);
// },
// child: const Text('确定'),
// ),
// ],
// ),
// );
}
}