Browse Source

feat(meeting): 优化悬浮窗权限请求逻辑

添加会话级别的权限请求状态管理,防止重复弹窗
增加权限请求冷却时间和对话框显示状态检查
添加设置跳转失败时的snackbar提示
weicu
lxm 9 months ago
parent
commit
8652584451
  1. 83
      lib/core/utils/overlay_permission_util.dart
  2. 63
      lib/modules/meeting/controllers/meeting_record_controller.dart

83
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<bool> 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<bool> hasOverlayPermission() async {
@ -49,38 +70,42 @@ class OverlayPermissionUtil {
'overlayPermissionRecordingDesc', // 为了在录音过程中显示录音悬浮条(计时/暂停/继续),需要悬浮窗权限。
};
// 显示说明对话框
bool? userAgreed = await Get.dialog<bool>(
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<bool>(
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();

63
lib/modules/meeting/controllers/meeting_record_controller.dart

@ -41,6 +41,9 @@ class MeetingRecordController extends GetxController
Timer? _routeMonitorTimer;
StreamSubscription<String>? _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<bool> _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<void> _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;

Loading…
Cancel
Save