From 4812ee55884db046df9ea62f2a02fa47e2b7dcd7 Mon Sep 17 00:00:00 2001 From: lxm <1274844347@qq.com> Date: Fri, 9 Jan 2026 19:44:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=BD=95=E9=9F=B3):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=90=8E=E5=8F=B0=E5=BD=95=E9=9F=B3=E9=80=9A=E7=9F=A5=E5=92=8C?= =?UTF-8?q?=E5=89=8D=E5=8F=B0=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加后台录音时的持续通知显示 实现Android前台服务保持录音进程 添加iOS本地通知提醒用户录音进行中 --- lib/core/translations/language/en_us.dart | 3 + lib/core/translations/language/zh_cn.dart | 2 + .../meeting_record_controller.dart | 65 +++++++++++++++++++ .../meeting/views/meeting_record_view.dart | 1 + .../azure_speech/AzureSpeechPlugin.kt | 17 ++++- 5 files changed, 87 insertions(+), 1 deletion(-) diff --git a/lib/core/translations/language/en_us.dart b/lib/core/translations/language/en_us.dart index f9fef5ba7..2a9a6cd8c 100644 --- a/lib/core/translations/language/en_us.dart +++ b/lib/core/translations/language/en_us.dart @@ -443,6 +443,9 @@ const Map enUS = { 'meetingRecordEndWithoutSave': 'End recording without saving', // 结束录音不保存 'meetingRecordContinueInBackground': 'Continue recording in background', // 后台继续录音 + 'meetingRecordRecordingNotificationTitle': 'Recording in progress', // 录音进行中 + 'meetingRecordRecordingNotificationBody': + 'Recording in background. Tap to return to the app.', // 正在后台录音,点击返回应用 'transcribing': 'Transcribing...', 'summarizing': 'Summarizing...', 'analyzing': 'Analyzing...', diff --git a/lib/core/translations/language/zh_cn.dart b/lib/core/translations/language/zh_cn.dart index afb01b8fa..2bffe4762 100644 --- a/lib/core/translations/language/zh_cn.dart +++ b/lib/core/translations/language/zh_cn.dart @@ -419,6 +419,8 @@ const Map zhCN = { 'meetingRecordEndAndSave': '结束录音并保存', // 结束录音并保存 'meetingRecordEndWithoutSave': '结束录音不保存', // 结束录音不保存 'meetingRecordContinueInBackground': '后台继续录音', // 后台继续录音 + 'meetingRecordRecordingNotificationTitle': '录音进行中', // 录音进行中 + 'meetingRecordRecordingNotificationBody': '正在后台录音,点击返回应用', // 正在后台录音,点击返回应用 'transcribing': '正在转写...', 'summarizing': '正在总结...', 'analyzing': '正在分析...', diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index ffcf6740c..70a998faf 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -11,6 +11,7 @@ import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import '../../../data/services/asr_service.dart'; import '../../../data/services/ble_manager.dart'; @@ -25,6 +26,66 @@ enum TimerStatus { /// 负责会议记录的状态管理、音频处理、语音识别和UI交互 class MeetingRecordController extends GetxController with GetTickerProviderStateMixin { + static const int _backgroundRecordingNotificationId = 91001; + + static FlutterLocalNotificationsPlugin? _localNotificationsPlugin; + + Future + _ensureLocalNotificationsPlugin() async { + final existing = _localNotificationsPlugin; + if (existing != null) return existing; + + final plugin = FlutterLocalNotificationsPlugin(); + const androidSettings = + AndroidInitializationSettings('@mipmap/ic_launcher'); + const iosSettings = DarwinInitializationSettings(); + const settings = InitializationSettings( + android: androidSettings, + iOS: iosSettings, + ); + await plugin.initialize(settings); + _localNotificationsPlugin = plugin; + return plugin; + } + + Future _requestNotificationPermissionIfNeeded() async { + await PermissionUtil.instance.requestPermission( + permissionType: Permission.notification, + permissionName: 'notificationPermission'.tr, + explanationText: 'notificationPermissionExplanation'.tr, + permanentDenialText: 'notificationPermissionPermanentDenial'.tr, + settingsButtonText: 'goToSettings'.tr, + cancelButtonText: 'cancel'.tr, + ); + } + + Future showBackgroundRecordingIndicator() async { + await _requestNotificationPermissionIfNeeded(); + if (!Platform.isIOS) return; + + final plugin = await _ensureLocalNotificationsPlugin(); + final details = NotificationDetails( + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: false, + presentSound: false, + ), + ); + + await plugin.show( + _backgroundRecordingNotificationId, + 'meetingRecordRecordingNotificationTitle'.tr, + 'meetingRecordRecordingNotificationBody'.tr, + details, + ); + } + + Future clearBackgroundRecordingIndicator() async { + if (!Platform.isIOS) return; + final plugin = await _ensureLocalNotificationsPlugin(); + await plugin.cancel(_backgroundRecordingNotificationId); + } + // 服务 final AsrService _asrService = Get.find(); final BleManager _bleManager = Get.find(); @@ -225,6 +286,8 @@ class MeetingRecordController extends GetxController return; } + await _requestNotificationPermissionIfNeeded(); + // 根据当前计时器状态执行相应操作 switch (timerStatus.value) { case TimerStatus.stopped: @@ -389,6 +452,7 @@ class MeetingRecordController extends GetxController } if (!isRecording.value) return; await _asrService.stopRecord(true); + await clearBackgroundRecordingIndicator(); // 如果标题被修改则重命名文件 if (fileName.value != newName.value) { @@ -427,6 +491,7 @@ class MeetingRecordController extends GetxController if (isRecording.value) { await _asrService.stopRecord(false); } + await clearBackgroundRecordingIndicator(); _resetToDefaultState(); isRecording.value = false; rms.value = 0.0; diff --git a/lib/modules/meeting/views/meeting_record_view.dart b/lib/modules/meeting/views/meeting_record_view.dart index 9c2a74634..b5a285ecd 100644 --- a/lib/modules/meeting/views/meeting_record_view.dart +++ b/lib/modules/meeting/views/meeting_record_view.dart @@ -226,6 +226,7 @@ class MeetingRecordView extends GetView { return; case _MeetingRecordExitAction.continueInBackground: await controller.saveTitleChange(); + await controller.showBackgroundRecordingIndicator(); Get.back(result: 'meeting_record_background'); return; case _MeetingRecordExitAction.cancel: diff --git a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt index c5846713c..ff536ae4a 100644 --- a/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt +++ b/local_plugins/azure_speech/android/src/main/kotlin/com/yunqiinnovation/azure_speech/AzureSpeechPlugin.kt @@ -5,6 +5,7 @@ import android.os.Handler import android.os.Looper import androidx.annotation.NonNull import com.yunqiinnovation.azure_speech.utils.FileLogger +import com.yunqiinnovation.azure_speech.tools.AudioRecordingForegroundService import com.yunqiinnovation.azure_speech.tools.SimpleAudioReceiver import com.yunqiinnovation.azure_speech.tools.SimpleAudioPlayer import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -581,6 +582,12 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { // 启用录音,并根据 acceptAudioData 参数决定是否设置音频数据回调 } FileLogger.d(tag, "音频文件名称为: ${filePath}") + + try { + AudioRecordingForegroundService.startService(context) + } catch (e: Exception) { + FileLogger.w(tag, "启动录音前台服务失败: ${e.message}") + } azureAsrHelper.enableRecord(audioSourceType, filePath, if(acceptAudioData) { // 创建音频数据回调,将音频数据发送到 Flutter 层 @@ -631,12 +638,20 @@ class AzureSpeechPlugin : BleService.Callback, FlutterPlugin { try { if(azureAsrHelper.audioSourceType == AzureAsrHelper.AudioSourceType.EXTERNAL){ isRecord = false - recordfile?.closeFile(true) + recordfile?.closeFile(isSave) } else { azureAsrHelper.stopRecord(isSave) } + + if (!azureAsrHelper.isContinuousRecognitionActive()) { + try { + AudioRecordingForegroundService.stopService(context) + } catch (e: Exception) { + FileLogger.w(tag, "停止录音前台服务失败: ${e.message}") + } + } result.success(true) } catch (e: Exception) { result.error("ENABLERECORD_ERROR", e.message, null)