From 8a5a2647c3ed202d17dba5e66d74af1f1f668b28 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Tue, 13 Jan 2026 16:36:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(meeting):=20=E4=BC=98=E5=8C=96=E6=82=AC?= =?UTF-8?q?=E6=B5=AE=E7=AA=97=E6=9D=83=E9=99=90=E8=AF=B7=E6=B1=82=E9=80=BB?= =?UTF-8?q?=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加会话级别的权限请求状态管理,防止重复弹窗 增加权限请求冷却时间和对话框显示状态检查 添加设置跳转失败时的snackbar提示 --- lib/core/utils/overlay_permission_util.dart | 83 ++++++++++++------- .../meeting_record_controller.dart | 63 ++++++++++++-- 2 files changed, 110 insertions(+), 36 deletions(-) diff --git a/lib/core/utils/overlay_permission_util.dart b/lib/core/utils/overlay_permission_util.dart index a4beb20e9..298ba4638 100644 --- a/lib/core/utils/overlay_permission_util.dart +++ b/lib/core/utils/overlay_permission_util.dart @@ -13,6 +13,27 @@ enum OverlayPermissionScene { /// 悬浮窗权限管理工具类 class OverlayPermissionUtil { static const MethodChannel _channel = MethodChannel('overlay_permission'); + static bool _isRequestDialogShowing = false; + static DateTime? _lastRequestDialogAt; + static const Duration _requestDialogCooldown = Duration(seconds: 20); + + static Future openOverlayPermissionSettings() async { + if (!Platform.isAndroid) return true; + try { + final bool opened = + await _channel.invokeMethod('requestOverlayPermission'); + if (!opened) { + Get.snackbar( + 'permissionRequestError'.tr, + 'overlayPermissionOpenSettingsFailed'.tr, + ); + } + return opened; + } catch (e) { + Logger.error('跳转悬浮窗权限设置失败: $e'); + return false; + } + } /// 检查是否有悬浮窗权限 static Future hasOverlayPermission() async { @@ -49,38 +70,42 @@ class OverlayPermissionUtil { 'overlayPermissionRecordingDesc', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。 }; - // 显示说明对话框 - bool? userAgreed = await Get.dialog( - AlertDialog( - title: Text('overlayPermissionTitle'.tr), // 悬浮窗权限 - content: Text(explanationKey.tr), // 按场景显示不同说明文案 - actions: [ - TextButton( - onPressed: () => Get.back(result: false), - child: Text('cancel'.tr), // 取消 - ), - ElevatedButton( - onPressed: () => Get.back(result: true), - child: Text('goToSettings'.tr), // 去设置 - ), - ], - ), - barrierDismissible: false, - ); + final now = DateTime.now(); + if (_isRequestDialogShowing) return false; + final lastAt = _lastRequestDialogAt; + if (lastAt != null && now.difference(lastAt) < _requestDialogCooldown) { + return false; + } + + _isRequestDialogShowing = true; + _lastRequestDialogAt = now; + bool? userAgreed; + try { + userAgreed = await Get.dialog( + AlertDialog( + title: Text('overlayPermissionTitle'.tr), + content: Text(explanationKey.tr), + actions: [ + TextButton( + onPressed: () => Get.back(result: false), + child: Text('cancel'.tr), + ), + ElevatedButton( + onPressed: () => Get.back(result: true), + child: Text('goToSettings'.tr), + ), + ], + ), + barrierDismissible: false, + ); + } finally { + _isRequestDialogShowing = false; + } if (userAgreed == true) { try { - // 跳转系统设置页 - final bool opened = - await _channel.invokeMethod('requestOverlayPermission'); - if (!opened) { - Get.snackbar( - 'permissionRequestError'.tr, - 'overlayPermissionOpenSettingsFailed' - .tr, // 无法自动跳转到系统设置,请手动前往系统设置开启悬浮窗权限。 - ); - return false; - } + final bool opened = await openOverlayPermissionSettings(); + if (!opened) return false; // 等待用户操作完成后重新检查权限 await Future.delayed(const Duration(seconds: 1)); return await hasOverlayPermission(); diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 83377d4a2..88cbc1fc1 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -41,6 +41,9 @@ class MeetingRecordController extends GetxController Timer? _routeMonitorTimer; StreamSubscription? _recordingFloatingActionSubscription; bool _floatingBarManuallyClosed = false; + bool _overlayPermissionPromptedThisSession = false; + bool _overlayPermissionSnackShownThisSession = false; + bool _overlayPermissionRequesting = false; AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; String _lastKnownRoute = ''; @@ -152,13 +155,8 @@ class MeetingRecordController extends GetxController if (!isRecording.value) return; if (_floatingBarManuallyClosed) return; - final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); - if (!hasPermission) { - final granted = await OverlayPermissionUtil.requestOverlayPermission( - scene: OverlayPermissionScene.recordingFloatingBar, - ); - if (!granted) return; - } + final granted = await _ensureAndroidOverlayPermissionForFloatingBar(); + if (!granted) return; final typeText = _getRecordingTypeText(); final durationText = _formatTimeForFloatingBar(milliseconds.value); @@ -191,6 +189,54 @@ class MeetingRecordController extends GetxController }); } + void _showOverlayPermissionSnackOnce() { + if (_overlayPermissionSnackShownThisSession) return; + _overlayPermissionSnackShownThisSession = true; + Get.snackbar( + 'overlayPermissionTitle'.tr, + 'overlayPermissionRecordingDesc'.tr, + snackPosition: SnackPosition.BOTTOM, + duration: const Duration(seconds: 4), + mainButton: TextButton( + onPressed: () { + unawaited(OverlayPermissionUtil.openOverlayPermissionSettings()); + }, + child: Text('goToSettings'.tr), + ), + ); + } + + Future _ensureAndroidOverlayPermissionForFloatingBar() async { + if (!Platform.isAndroid) return true; + if (!isRecording.value) return false; + + final hasPermission = await OverlayPermissionUtil.hasOverlayPermission(); + if (hasPermission) return true; + + if (_appLifecycleState != AppLifecycleState.resumed) { + return false; + } + + if (_overlayPermissionPromptedThisSession || _overlayPermissionRequesting) { + _showOverlayPermissionSnackOnce(); + return false; + } + + _overlayPermissionPromptedThisSession = true; + _overlayPermissionRequesting = true; + try { + final granted = await OverlayPermissionUtil.requestOverlayPermission( + scene: OverlayPermissionScene.recordingFloatingBar, + ); + if (!granted) { + _showOverlayPermissionSnackOnce(); + } + return granted; + } finally { + _overlayPermissionRequesting = false; + } + } + Future _stopAndroidRecordingFloatingBar() async { if (!Platform.isAndroid) return; _recordingFloatingBarTimer?.cancel(); @@ -513,6 +559,9 @@ class MeetingRecordController extends GetxController }); isRecording.value = true; + _overlayPermissionPromptedThisSession = false; + _overlayPermissionSnackShownThisSession = false; + _overlayPermissionRequesting = false; // 更新录音状态 fileName.value = fullFileName; newName.value = fullFileName;