|
|
@ -1,93 +1,119 @@ |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:flutter/material.dart'; |
|
|
import 'package:get/get.dart'; |
|
|
|
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
import 'package:permission_handler/permission_handler.dart'; |
|
|
import 'package:app_settings/app_settings.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 { |
|
|
class PermissionUtil { |
|
|
/// 检查并请求多个权限 |
|
|
static final PermissionUtil instance = PermissionUtil._(); |
|
|
/// |
|
|
PermissionUtil._(); |
|
|
/// [permissions] 要检查的权限列表,如 [Permission.microphone, Permission.location] |
|
|
|
|
|
/// [rationaleTitle] 弹窗标题 |
|
|
OverlayEntry? _explanationOverlayEntry; |
|
|
/// [rationaleMessage] 权限用途说明内容(支持多行) |
|
|
|
|
|
/// |
|
|
Future<bool> requestPermission({ |
|
|
/// 返回是否所有权限都已被授予 |
|
|
required BuildContext context, |
|
|
static Future<bool> checkAndRequestPermissions({ |
|
|
required Permission permissionType, |
|
|
required List<Permission> permissions, |
|
|
required String permissionName, |
|
|
required String rationaleTitle, |
|
|
required String explanationText, |
|
|
required List<String> rationaleMessage, |
|
|
required String permanentDenialText, |
|
|
|
|
|
String settingsButtonText = '前往设置', |
|
|
|
|
|
String cancelButtonText = '取消', |
|
|
}) async { |
|
|
}) async { |
|
|
try { |
|
|
try { |
|
|
Map<Permission, PermissionStatus> statuses = {}; |
|
|
final PermissionStatus status = await permissionType.status; |
|
|
|
|
|
log('初始权限状态 for $permissionName: $status'); |
|
|
// 检查是否都已授权 |
|
|
|
|
|
bool allGranted = true; |
|
|
// 处理已授权状态(包括授予状态和临时通知权限) |
|
|
for (var permission in permissions) { |
|
|
if (status == PermissionStatus.granted || |
|
|
final status = await permission.status; |
|
|
status == PermissionStatus.provisional) { |
|
|
if (!status.isGranted) { |
|
|
log('$permissionName 权限已授予或临时授权。'); |
|
|
allGranted = false; |
|
|
return true; |
|
|
statuses[permission] = status; |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 处理永久拒绝状态 |
|
|
|
|
|
if (status == PermissionStatus.permanentlyDenied) { |
|
|
|
|
|
log('$permissionName 权限被永久拒绝,显示引导对话框。'); |
|
|
|
|
|
await _showPermanentDenialDialog( |
|
|
|
|
|
context: context, |
|
|
|
|
|
permissionName: permissionName, |
|
|
|
|
|
permanentDenialText: permanentDenialText, |
|
|
|
|
|
settingsButtonText: settingsButtonText, |
|
|
|
|
|
cancelButtonText: cancelButtonText, |
|
|
|
|
|
); |
|
|
|
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
if (allGranted) return true; |
|
|
// 处理初始拒绝/未决定状态 |
|
|
|
|
|
if (status == PermissionStatus.denied) { |
|
|
|
|
|
log('$permissionName 权限被拒绝或未决定,显示说明横幅并请求。'); |
|
|
|
|
|
_showExplanationOverlay( |
|
|
|
|
|
context: context, |
|
|
|
|
|
permissionName: permissionName, |
|
|
|
|
|
explanationText: explanationText, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
// 判断是否包含永久拒绝 |
|
|
final PermissionStatus newStatus = await permissionType.request(); |
|
|
bool hasPermanentDenied = |
|
|
log('请求后 $permissionName 的新权限状态: $newStatus'); |
|
|
statuses.values.any((status) => status.isPermanentlyDenied); |
|
|
|
|
|
|
|
|
|
|
|
// 显示提示弹窗 |
|
|
_hideExplanationOverlay(); |
|
|
_showPermissionDialog( |
|
|
|
|
|
isPermanent: hasPermanentDenied, |
|
|
// 处理请求后的状态 |
|
|
title: rationaleTitle, |
|
|
if (newStatus == PermissionStatus.granted || |
|
|
messages: rationaleMessage, |
|
|
newStatus == PermissionStatus.provisional) { |
|
|
onConfirm: () async { |
|
|
log('$permissionName 权限在请求后被授予或临时授权。'); |
|
|
if (hasPermanentDenied) { |
|
|
return true; |
|
|
AppSettings.openAppSettings(); |
|
|
} else if (newStatus == PermissionStatus.permanentlyDenied) { |
|
|
} else { |
|
|
log('$permissionName 权限在请求后被永久拒绝,再次显示引导对话框。'); |
|
|
final newStatuses = await permissions.request(); |
|
|
await _showPermanentDenialDialog( |
|
|
final allGrantedNow = |
|
|
context: context, |
|
|
newStatuses.values.every((status) => status.isGranted); |
|
|
permissionName: permissionName, |
|
|
if (!allGrantedNow) { |
|
|
permanentDenialText: permanentDenialText, |
|
|
print("部分权限被拒绝: $newStatuses"); |
|
|
settingsButtonText: settingsButtonText, |
|
|
|
|
|
cancelButtonText: cancelButtonText, |
|
|
|
|
|
); |
|
|
} |
|
|
} |
|
|
|
|
|
return false; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// 处理受限状态 |
|
|
|
|
|
if (status == PermissionStatus.restricted || |
|
|
|
|
|
status == PermissionStatus.limited) { |
|
|
|
|
|
log('$permissionName 权限受限或受限访问: $status'); |
|
|
|
|
|
await _showRestrictedPermissionDialog(context, permissionName); |
|
|
|
|
|
return false; |
|
|
} |
|
|
} |
|
|
}, |
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 其他未知状态处理 |
|
|
|
|
|
log('$permissionName 处于未知状态: $status'); |
|
|
return false; |
|
|
return false; |
|
|
} catch (e) { |
|
|
} catch (e) { |
|
|
print("权限检查异常: $e"); |
|
|
Logger.e('PermissionUtil', '请求 $permissionName 权限失败: $e'); |
|
|
|
|
|
_hideExplanationOverlay(); |
|
|
return false; |
|
|
return false; |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
/// 权限说明弹窗 |
|
|
void _showExplanationOverlay({ |
|
|
static void _showPermissionDialog({ |
|
|
required BuildContext context, |
|
|
required bool isPermanent, |
|
|
required String permissionName, |
|
|
required String title, |
|
|
required String explanationText, |
|
|
required List<String> messages, |
|
|
|
|
|
required VoidCallback onConfirm, |
|
|
|
|
|
}) { |
|
|
}) { |
|
|
Get.generalDialog( |
|
|
_hideExplanationOverlay(); // 确保移除旧叠加层 |
|
|
barrierDismissible: false, |
|
|
|
|
|
barrierLabel: '权限提示', |
|
|
_explanationOverlayEntry = OverlayEntry( |
|
|
transitionBuilder: (context, anim1, anim2, child) { |
|
|
builder: (context) => Positioned( |
|
|
return SlideTransition( |
|
|
top: MediaQuery.of(context).padding.top + 10, |
|
|
position: Tween<Offset>( |
|
|
left: 16, |
|
|
begin: Offset(0, -1), |
|
|
right: 16, |
|
|
end: Offset(0, 0), |
|
|
|
|
|
).animate(CurvedAnimation(parent: anim1, curve: Curves.easeOut)), |
|
|
|
|
|
child: child, |
|
|
|
|
|
); |
|
|
|
|
|
}, |
|
|
|
|
|
pageBuilder: (context, anim1, anim2) { |
|
|
|
|
|
return SafeArea( |
|
|
|
|
|
child: Align( |
|
|
|
|
|
alignment: Alignment.topCenter, |
|
|
|
|
|
child: Padding( |
|
|
|
|
|
padding: const EdgeInsets.only(top: 80, left: 24, right: 24), |
|
|
|
|
|
child: Material( |
|
|
child: Material( |
|
|
borderRadius: BorderRadius.circular(12), |
|
|
borderRadius: BorderRadius.circular(12), |
|
|
|
|
|
elevation: 4, |
|
|
color: Colors.white, |
|
|
color: Colors.white, |
|
|
child: Padding( |
|
|
child: Padding( |
|
|
padding: const EdgeInsets.all(16), |
|
|
padding: const EdgeInsets.all(16), |
|
|
@ -95,40 +121,82 @@ class PermissionUtil { |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
mainAxisSize: MainAxisSize.min, |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
crossAxisAlignment: CrossAxisAlignment.start, |
|
|
children: [ |
|
|
children: [ |
|
|
Text(title, |
|
|
Text( |
|
|
style: TextStyle( |
|
|
'需要$permissionName权限', |
|
|
fontSize: 18, fontWeight: FontWeight.bold)), |
|
|
style: const TextStyle( |
|
|
const SizedBox(height: 12), |
|
|
fontWeight: FontWeight.bold, |
|
|
...messages.map((msg) => Padding( |
|
|
fontSize: 16, |
|
|
padding: const EdgeInsets.only(bottom: 8), |
|
|
|
|
|
child: Text("• $msg"), |
|
|
|
|
|
)), |
|
|
|
|
|
const SizedBox(height: 20), |
|
|
|
|
|
Row( |
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.end, |
|
|
|
|
|
children: [ |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () => Get.back(), |
|
|
|
|
|
child: const Text('取消'), |
|
|
|
|
|
), |
|
|
), |
|
|
const SizedBox(width: 8), |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
Get.back(); |
|
|
|
|
|
onConfirm(); |
|
|
|
|
|
}, |
|
|
|
|
|
child: Text(isPermanent ? '前往设置' : '授权'), |
|
|
|
|
|
), |
|
|
), |
|
|
], |
|
|
const SizedBox(height: 8), |
|
|
|
|
|
Text( |
|
|
|
|
|
explanationText, |
|
|
|
|
|
style: const TextStyle(fontSize: 14), |
|
|
), |
|
|
), |
|
|
], |
|
|
], |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
), |
|
|
|
|
|
); |
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
Future.delayed(Duration.zero, () { |
|
|
|
|
|
if (_explanationOverlayEntry != null) { |
|
|
|
|
|
Overlay.of(context).insert(_explanationOverlayEntry!); |
|
|
|
|
|
log('权限说明叠加层已显示。'); |
|
|
|
|
|
} |
|
|
|
|
|
}); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
void _hideExplanationOverlay() { |
|
|
|
|
|
_explanationOverlayEntry?.remove(); |
|
|
|
|
|
_explanationOverlayEntry = null; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _showPermanentDenialDialog({ |
|
|
|
|
|
required BuildContext context, |
|
|
|
|
|
required String permissionName, |
|
|
|
|
|
required String permanentDenialText, |
|
|
|
|
|
required String settingsButtonText, |
|
|
|
|
|
required String cancelButtonText, |
|
|
|
|
|
}) async { |
|
|
|
|
|
await showDialog<void>( |
|
|
|
|
|
context: context, |
|
|
|
|
|
barrierDismissible: false, |
|
|
|
|
|
builder: (context) => AlertDialog( |
|
|
|
|
|
title: Text('需要$permissionName权限'), |
|
|
|
|
|
content: Text(permanentDenialText), |
|
|
|
|
|
actions: [ |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () => Navigator.pop(context), |
|
|
|
|
|
child: Text(cancelButtonText), |
|
|
|
|
|
), |
|
|
|
|
|
ElevatedButton( |
|
|
|
|
|
onPressed: () { |
|
|
|
|
|
Navigator.pop(context); |
|
|
|
|
|
AppSettings.openAppSettings(); |
|
|
}, |
|
|
}, |
|
|
|
|
|
child: Text(settingsButtonText), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
|
|
|
); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Future<void> _showRestrictedPermissionDialog( |
|
|
|
|
|
BuildContext context, String permissionName) async { |
|
|
|
|
|
await showDialog( |
|
|
|
|
|
context: context, |
|
|
|
|
|
builder: (context) => AlertDialog( |
|
|
|
|
|
title: Text('$permissionName权限受限'), |
|
|
|
|
|
content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'), |
|
|
|
|
|
actions: [ |
|
|
|
|
|
TextButton( |
|
|
|
|
|
onPressed: () => Navigator.pop(context), |
|
|
|
|
|
child: const Text('确定'), |
|
|
|
|
|
), |
|
|
|
|
|
], |
|
|
|
|
|
), |
|
|
); |
|
|
); |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|