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"
diff --git a/lib/core/utils/permission_util.dart b/lib/core/utils/permission_util.dart
index e51aafb3f..0961e7120 100644
--- a/lib/core/utils/permission_util.dart
+++ b/lib/core/utils/permission_util.dart
@@ -1,21 +1,40 @@
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');
-// }
-// }
+/// 权限请求工具类
+///
+/// 提供统一的权限请求流程处理:
+/// 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,
@@ -26,79 +45,108 @@ class PermissionUtil {
String cancelButtonText = '取消',
}) 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;
}
- // 处理永久拒绝状态
+ // 显示权限解释横幅(覆盖所有状态的处理)
+ _showExplanationOverlay(
+ context: context,
+ permissionName: permissionName,
+ explanationText: explanationText,
+ );
+
+ // 处理永久拒绝状态(先显示说明横幅,再引导用户去设置)
if (status == PermissionStatus.permanentlyDenied) {
- log('$permissionName 权限被永久拒绝,显示引导对话框。');
- await _showPermanentDenialDialog(
+ Logger.w('PermissionUtil', '$permissionName 权限被永久拒绝,显示引导对话框');
+
+ // 显示永久拒绝对话框(在解释横幅下方显示)
+ final result = await _showPermanentDenialDialog(
context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
);
- return false;
+ // 移除解释横幅
+ _hideExplanationOverlay();
+ return result;
}
// 处理初始拒绝/未决定状态
if (status == PermissionStatus.denied) {
- log('$permissionName 权限被拒绝或未决定,显示说明横幅并请求。');
- _showExplanationOverlay(
- context: context,
- permissionName: permissionName,
- explanationText: explanationText,
- );
-
+ Logger.d('PermissionUtil', '$permissionName 权限被拒绝或未决定,显示说明横幅并请求');
+ // _showExplanationOverlay(
+ // context: context,
+ // permissionName: permissionName,
+ // explanationText: explanationText,
+ // );
+//请求权限
final PermissionStatus newStatus = await permissionType.request();
- log('请求后 $permissionName 的新权限状态: $newStatus');
+ Logger.d('PermissionUtil', '请求后 $permissionName 的新权限状态: $newStatus');
- _hideExplanationOverlay();
+ //_hideExplanationOverlay();
// 处理请求后的状态
if (newStatus == PermissionStatus.granted ||
newStatus == PermissionStatus.provisional) {
- log('$permissionName 权限在请求后被授予或临时授权。');
+ Logger.i('PermissionUtil', '$permissionName 权限在请求后被授予');
+ _hideExplanationOverlay();
return true;
- } else if (newStatus == PermissionStatus.permanentlyDenied) {
- log('$permissionName 权限在请求后被永久拒绝,再次显示引导对话框。');
- await _showPermanentDenialDialog(
+ }
+ // 处理请求后变为永久拒绝
+ else if (newStatus == PermissionStatus.permanentlyDenied) {
+ Logger.w('PermissionUtil', '$permissionName 权限在请求后被永久拒绝');
+ final result = await _showPermanentDenialDialog(
context: context,
permissionName: permissionName,
permanentDenialText: permanentDenialText,
settingsButtonText: settingsButtonText,
cancelButtonText: cancelButtonText,
);
+ _hideExplanationOverlay();
+ return result;
}
+ // 其他拒绝状态
+ _hideExplanationOverlay();
return false;
}
// 处理受限状态
if (status == PermissionStatus.restricted ||
status == PermissionStatus.limited) {
- log('$permissionName 权限受限或受限访问: $status');
+ Logger.w('PermissionUtil', '$permissionName 权限受限或受限访问: $status');
await _showRestrictedPermissionDialog(context, permissionName);
+ _hideExplanationOverlay();
return false;
}
// 其他未知状态处理
- log('$permissionName 处于未知状态: $status');
+ Logger.e('PermissionUtil', '$permissionName 处于未知状态: $status');
+ _hideExplanationOverlay();
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,
@@ -113,8 +161,9 @@ class PermissionUtil {
right: 16,
child: Material(
borderRadius: BorderRadius.circular(12),
- elevation: 4,
- color: Colors.white,
+ elevation: 8,
+ color: Theme.of(context).colorScheme.background,
+ shadowColor: Colors.black.withOpacity(0.3),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
@@ -143,24 +192,33 @@ 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({
+ /// 显示永久拒绝后的设置引导对话框
+ ///
+ /// 在权限被永久拒绝时调用,提示用户前往设置手动开启权限
+ Future _showPermanentDenialDialog({
required BuildContext context,
required String permissionName,
required String permanentDenialText,
required String settingsButtonText,
required String cancelButtonText,
}) async {
- await showDialog(
+ Logger.i('PermissionUtil', '显示永久拒绝对话框 ($permissionName)');
+
+ bool? userChoice = await showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
@@ -168,23 +226,39 @@ class PermissionUtil {
content: Text(permanentDenialText),
actions: [
TextButton(
- onPressed: () => Navigator.pop(context),
+ onPressed: () {
+ Logger.d('PermissionUtil', '用户取消设置 ($permissionName)');
+ Navigator.pop(context, false);
+ },
child: Text(cancelButtonText),
),
ElevatedButton(
onPressed: () {
- Navigator.pop(context);
- AppSettings.openAppSettings();
+ Logger.d('PermissionUtil', '用户同意前往设置 ($permissionName)');
+ Navigator.pop(context, true);
},
child: Text(settingsButtonText),
),
],
),
);
+
+ // 用户选择前往设置
+ if (userChoice == true) {
+ AppSettings.openAppSettings();
+ return false; // 不能立即获取权限,需要用户手动设置
+ }
+
+ // 用户取消
+ return false;
}
+ /// 显示受限权限对话框
+ ///
+ /// 在权限受限状态(如iOS上的受限照片访问)时调用
Future _showRestrictedPermissionDialog(
BuildContext context, String permissionName) async {
+ Logger.w('PermissionUtil', '显示受限权限对话框 ($permissionName)');
await showDialog(
context: context,
builder: (context) => AlertDialog(
@@ -192,7 +266,10 @@ class PermissionUtil {
content: const Text('此功能在当前设备上不可用或受限,请检查设备设置或系统限制'),
actions: [
TextButton(
- onPressed: () => Navigator.pop(context),
+ onPressed: () {
+ Logger.d('PermissionUtil', '用户确认受限权限 ($permissionName)');
+ Navigator.pop(context);
+ },
child: const Text('确定'),
),
],
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(