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'; import 'package:get_storage/get_storage.dart'; /// 权限请求工具类 /// /// 提供统一的权限请求流程处理: /// 1. 初始权限状态检查 /// 2. 权限解释横幅显示(悬浮在UI顶部的解释性UI) /// 3. 权限请求执行 /// 4. 永久拒绝后的设置引导 /// 5. 智能拒绝次数统计和熔断机制 /// /// 使用单例模式:`PermissionUtil.instance` class PermissionUtil { /// 单例实例 static final PermissionUtil instance = PermissionUtil._(); /// 私有构造函数 PermissionUtil._(); /// 权限解释横幅的叠加层入口 //OverlayEntry? _explanationOverlayEntry; /// 引入 get_storage 用于持久化权限拒绝次数 final _storage = GetStorage(); /// 权限拒绝次数阈值(达到此次数后视为永久拒绝) static const int _denialThreshold = 2; /// 获取权限拒绝次数的存储键 String _getDenialCountKey(Permission permission) { return 'permission_denial_count_${permission.toString()}'; } /// 重置指定权限的拒绝次数 /// /// 在某些场景下可能需要手动重置拒绝次数,比如用户主动要求重新请求权限 Future resetDenialCount(Permission permissionType) async { await _storage.remove(_getDenialCountKey(permissionType)); Logger.d('PermissionUtil', '已重置 ${permissionType.toString()} 的拒绝次数'); } /// 获取指定权限的拒绝次数 int getDenialCount(Permission permissionType) { return _storage.read(_getDenialCountKey(permissionType)) ?? 0; } /// 请求指定权限 /// /// 参数: /// [context]: BuildContext,用于UI显示和导航 /// [permissionType]: 要请求的权限类型(Permission.microphone等) /// [permissionName]: 权限的用户友好名称(如"麦克风"、"定位") /// [explanationText]: 权限用途的详细解释文本 /// [permanentDenialText]: 永久拒绝后引导用户去设置的文案 /// [settingsButtonText]: "去设置"按钮的文本(可选,默认"前往设置") /// [cancelButtonText]: "取消"按钮的文本(可选,默认"取消") /// /// 返回值:`Future` 权限是否最终授予 Future 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 权限已授予或临时授权'); // 如果已经授权,清零之前的拒绝记录,以防用户在设置中开启后又关闭 await _storage.remove(_getDenialCountKey(permissionType)); return true; } // 2. 读取历史拒绝次数 final denialCountKey = _getDenialCountKey(permissionType); int denialCount = _storage.read(denialCountKey) ?? 0; Logger.d('PermissionUtil', '$permissionName 当前拒绝次数: $denialCount'); // 3. 熔断机制:如果拒绝次数达到阈值或系统返回了 permanentlyDenied,直接视为永久拒绝 if (denialCount >= _denialThreshold || status == PermissionStatus.permanentlyDenied) { Logger.w('PermissionUtil', '$permissionName 已被拒绝 $denialCount 次或被系统永久拒绝,触发设置引导'); // 显示权限解释横幅 _showExplanationOverlay( permissionName: permissionName, explanationText: explanationText, ); // 显示引导去设置的对话框 final result = await _showPermanentDenialDialog( permissionName: permissionName, permanentDenialText: permanentDenialText, settingsButtonText: finalSettingsButtonText, cancelButtonText: finalCancelButtonText, ); // 移除解释横幅 _hideExplanationOverlay(); return result; } // 显示权限解释横幅(覆盖所有状态的处理) _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.restricted || status == PermissionStatus.limited) { Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status'); await _showRestrictedPermissionDialog(permissionName); _hideExplanationOverlay(); return false; } // 处理初始拒绝/未决定状态(真正的权限请求) 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(); // 5. 根据结果更新状态和拒绝次数 if (newStatus == PermissionStatus.granted || newStatus == PermissionStatus.provisional) { Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予'); // 授权成功,清零拒绝次数 await _storage.remove(denialCountKey); _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; } // 如果执行到这里,说明权限被拒绝了 (denied) else { Logger.w('PermissionUtil', '$permissionName 权限被拒绝'); // 拒绝次数 +1 await _storage.write(denialCountKey, denialCount + 1); Logger.d( 'PermissionUtil', '$permissionName 拒绝次数更新为: ${denialCount + 1}'); _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 _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( // context: context, // barrierDismissible: false, // builder: (context) => // ); // 用户选择前往设置 if (userChoice == true) { AppSettings.openAppSettings(); return false; // 不能立即获取权限,需要用户手动设置 } // 用户取消 return false; } /// 显示受限权限对话框 /// /// 在权限受限状态(如iOS上的受限照片访问)时调用 Future _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('确定'), // ), // ], // ), // ); } }