Browse Source

feat(ble): 实现BLE唤醒策略的集中管理

重构BLE唤醒功能的控制逻辑,将其从各页面迁移至AppLifecycleManager统一管理
添加路由检测和录音任务检测来自动更新唤醒策略
优化错误处理和日志记录
newdev_shunjiawei
lxm 9 months ago
parent
commit
fef859e0d3
  1. 52
      lib/core/utils/app_lifecycle_manager.dart
  2. 11
      lib/main.dart
  3. 35
      lib/modules/home/controllers/home_controller.dart
  4. 16
      lib/modules/meeting/views/meeting_record_view.dart
  5. 9
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  6. 8
      local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift
  7. 8
      local_plugins/agent_service/lib/agent_service.dart

52
lib/core/utils/app_lifecycle_manager.dart

@ -1,6 +1,11 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:agent_service/agent_service.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart'; import 'package:get_storage/get_storage.dart';
import '../../modules/meeting/controllers/meeting_record_controller.dart';
import '../../routes/app_routes.dart';
import 'logger.dart'; import 'logger.dart';
import '../translations/app_translations.dart'; import '../translations/app_translations.dart';
//import '../../modules/settings/controllers/settings_controller.dart'; //import '../../modules/settings/controllers/settings_controller.dart';
@ -23,17 +28,19 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
final RxBool _isInForeground = true.obs; final RxBool _isInForeground = true.obs;
bool get isInForeground => _isInForeground.value; bool get isInForeground => _isInForeground.value;
bool? _lastCanWakeUp;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
// 注册应用生命周期观察者 // 注册应用生命周期观察者
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
Logger.info('AppLifecycleManager 已初始化'); Logger.info('AppLifecycleManager 已初始化');
unawaited(refreshBleWakeupPolicy());
} }
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
final previousState = _appLifecycleState.value;
_appLifecycleState.value = state; _appLifecycleState.value = state;
_lifecycleStateStream.value = state; _lifecycleStateStream.value = state;
@ -44,6 +51,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
_isInForeground.value = true; _isInForeground.value = true;
_handleAppResumed(); _handleAppResumed();
unawaited(refreshBleWakeupPolicy());
break; break;
case AppLifecycleState.paused: case AppLifecycleState.paused:
case AppLifecycleState.inactive: case AppLifecycleState.inactive:
@ -56,6 +64,48 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
} }
} }
Future<void> refreshBleWakeupPolicy({String? route}) async {
final currentRoute = route ?? Get.currentRoute;
final bool hasMeetingRecordingTask = _hasMeetingRecordingTask();
final bool shouldDisableWakeup = hasMeetingRecordingTask ||
_isMeetingRoute(currentRoute) ||
_isTranslationRoute(currentRoute);
final bool canWakeUp = !shouldDisableWakeup;
if (_lastCanWakeUp == canWakeUp) return;
_lastCanWakeUp = canWakeUp;
try {
Logger.info('lxm---设置是否可以ble唤醒agent: $canWakeUp');
await AgentService.setCanWakeUp(canWakeUp);
} catch (e) {
Logger.warning('setCanWakeUp失败: $e');
}
}
bool _isMeetingRoute(String route) {
if (route.isEmpty) return false;
if (route == Routes.profileMeeting) return true;
return route == Routes.meeting || route.startsWith('${Routes.meeting}/');
}
bool _isTranslationRoute(String route) {
if (route.isEmpty) return false;
if (route == Routes.translationMiddleware) return true;
if (route == Routes.pictureTranslation) return true;
if (route == Routes.textTranslation) return true;
return route == Routes.translation ||
route.startsWith('${Routes.translation}/');
}
bool _hasMeetingRecordingTask() {
if (!Get.isRegistered<MeetingRecordController>()) return false;
final mrc = Get.find<MeetingRecordController>();
return mrc.isRecording.value ||
mrc.timerStatus.value != TimerStatus.stopped;
}
/// 处理应用进入前台 /// 处理应用进入前台
void _handleAppResumed() { void _handleAppResumed() {
// Logger.info('应用进入前台'); // Logger.info('应用进入前台');

11
lib/main.dart

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
@ -8,6 +10,7 @@ import 'package:get_storage/get_storage.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'core/utils/app_lifecycle_manager.dart';
import 'routes/app_pages.dart'; import 'routes/app_pages.dart';
import 'routes/app_routes.dart'; import 'routes/app_routes.dart';
import 'core/translations/app_translations.dart'; import 'core/translations/app_translations.dart';
@ -127,6 +130,14 @@ class MainApp extends StatelessWidget {
initialBinding: InitialBinding(), initialBinding: InitialBinding(),
initialRoute: Routes.splash, initialRoute: Routes.splash,
getPages: AppPages.pages, getPages: AppPages.pages,
routingCallback: (routing) {
if (!Get.isRegistered<AppLifecycleManager>()) return;
unawaited(
Get.find<AppLifecycleManager>().refreshBleWakeupPolicy(
route: routing?.current,
),
);
},
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
translations: AppTranslations(), translations: AppTranslations(),
localizationsDelegates: const [ localizationsDelegates: const [

35
lib/modules/home/controllers/home_controller.dart

@ -11,7 +11,6 @@ import '../../../data/services/ble_manager.dart';
import '../../../data/services/ota_check_service.dart'; import '../../../data/services/ota_check_service.dart';
import '../../../data/models/appconfig.dart'; import '../../../data/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart'; import '../../../data/models/appconfig_model.dart';
import '../../meeting/controllers/meeting_record_controller.dart';
/// 连接状态枚举 /// 连接状态枚举
enum ConnectionStatus { enum ConnectionStatus {
@ -241,13 +240,9 @@ class HomeController extends GetxController {
Get.toNamed(Routes.pairing); Get.toNamed(Routes.pairing);
} }
void openTranslation() async { void openTranslation() {
AgentService.interruptCurrentResponse(); AgentService.interruptCurrentResponse();
AgentService.setCanWakeUp(false); Get.toNamed(Routes.translationMiddleware);
Logger.d(_tag, 'liwei-----------关闭唤醒功能');
await Get.toNamed(Routes.translationMiddleware);
AgentService.setCanWakeUp(true);
Logger.d(_tag, 'liwei-----------打开唤醒功能');
} }
/// 打开会议模块 /// 打开会议模块
@ -255,31 +250,9 @@ class HomeController extends GetxController {
/// 返回值:`Future<void>` 无返回数据 /// 返回值:`Future<void>` 无返回数据
/// 异常:不显式抛出;内部仅记录日志 /// 异常:不显式抛出;内部仅记录日志
/// 说明:进入会议页前关闭唤醒;返回首页后根据录音状态决定是否开启唤醒(存在录音任务则不启用) /// 说明:进入会议页前关闭唤醒;返回首页后根据录音状态决定是否开启唤醒(存在录音任务则不启用)
void openMeet() async { void openMeet() {
AgentService.interruptCurrentResponse(); AgentService.interruptCurrentResponse();
AgentService.setCanWakeUp(false); Get.toNamed(Routes.meeting);
Logger.d(_tag, 'liwei-----------关闭唤醒功能');
await Get.toNamed(Routes.meeting);
try {
if (Get.isRegistered<MeetingRecordController>()) {
final mrc = Get.find<MeetingRecordController>();
final hasRecording = mrc.isRecording.value ||
mrc.timerStatus.value != TimerStatus.stopped;
if (hasRecording) {
AgentService.setCanWakeUp(false);
Logger.d(_tag, 'openMeet返回:检测到录音任务,保持关闭唤醒功能');
} else {
AgentService.setCanWakeUp(true);
Logger.d(_tag, 'openMeet返回:无录音任务,打开唤醒功能');
}
} else {
AgentService.setCanWakeUp(true);
Logger.d(_tag, 'openMeet返回:未注册MeetingRecordController,默认打开唤醒功能');
}
} catch (e) {
AgentService.setCanWakeUp(true);
Logger.e(_tag, 'openMeet返回:检测录音任务异常,默认打开唤醒功能: $e');
}
} }
void exploreAIFeatures() { void exploreAIFeatures() {

16
lib/modules/meeting/views/meeting_record_view.dart

@ -6,6 +6,7 @@ import 'package:get/get.dart';
import '../controllers/meeting_record_controller.dart'; import '../controllers/meeting_record_controller.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
/// 会议录音退出操作
enum _MeetingRecordExitAction { enum _MeetingRecordExitAction {
saveAndEnd, saveAndEnd,
endWithoutSave, endWithoutSave,
@ -13,6 +14,7 @@ enum _MeetingRecordExitAction {
cancel, cancel,
} }
/// 会议录音页面
class MeetingRecordView extends GetView<MeetingRecordController> { class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key}); const MeetingRecordView({super.key});
@ -24,6 +26,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
}); });
} }
/// 显示退出确认弹窗
Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) { Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) {
return Get.dialog<_MeetingRecordExitAction>( return Get.dialog<_MeetingRecordExitAction>(
Dialog( Dialog(
@ -58,7 +61,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
SizedBox(height: 14.h), SizedBox(height: 14.h),
Text( Text(
'meetingRecordExitTitle'.tr, // 对应中文:退出录音 'meetingRecordExitTitle'.tr, // 退出录音
style: TextStyle( style: TextStyle(
fontSize: 18.sp, fontSize: 18.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@ -67,7 +70,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
SizedBox(height: 8.h), SizedBox(height: 8.h),
Text( Text(
'meetingRecordExitMessage'.tr, // 对应中文:请选择离开后的操作 'meetingRecordExitMessage'.tr, // 请选择离开后的操作
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
@ -96,7 +99,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
elevation: 0, elevation: 0,
), ),
child: Text( child: Text(
'meetingRecordEndAndSave'.tr, // 对应中文:结束录音并保存 'meetingRecordEndAndSave'.tr, // 结束录音并保存
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -121,7 +124,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
), ),
child: Text( child: Text(
'meetingRecordContinueInBackground'.tr, // 对应中文:后台继续录音 'meetingRecordContinueInBackground'.tr, // 后台继续录音
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -146,7 +149,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
), ),
child: Text( child: Text(
'meetingRecordEndWithoutSave'.tr, // 对应中文:结束录音不保存 'meetingRecordEndWithoutSave'.tr, // 结束录音不保存
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -168,7 +171,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
), ),
), ),
child: Text( child: Text(
'cancel'.tr, // 对应中文:取消 'cancel'.tr, // 取消
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
@ -305,6 +308,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
); );
} }
/// 构建音频标签页
Widget _buildAudioTab(BuildContext context, bool isDarkMode) { Widget _buildAudioTab(BuildContext context, bool isDarkMode) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {

9
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift

@ -80,6 +80,7 @@ class AgentServiceImpl: NSObject {
private var isRecognizing: Bool = false private var isRecognizing: Bool = false
private var hasSpeechDetected: Bool = false private var hasSpeechDetected: Bool = false
internal var isAiStreaming: Bool = false internal var isAiStreaming: Bool = false
private var isCanWakeUp: Bool = true
// 新增:识别"启动中"与"启动中收到停止请求"状态 // 新增:识别"启动中"与"启动中收到停止请求"状态
// 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题 // 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题
@ -167,6 +168,10 @@ class AgentServiceImpl: NSObject {
private func sendError(_ message: String, code: String = "ERROR") { private func sendError(_ message: String, code: String = "ERROR") {
sendEvent(name: "error", data: ["code": code, "message": message]) sendEvent(name: "error", data: ["code": code, "message": message])
} }
func setCanWakeUp(_ canWakeUp: Bool) {
isCanWakeUp = canWakeUp
}
private func initSystemPrompt() { private func initSystemPrompt() {
systemPrompt = """ systemPrompt = """
@ -2025,7 +2030,9 @@ extension AgentServiceImpl: BleService.Callback {
func onWakeupSignalReceived() { func onWakeupSignalReceived() {
if !isCanWakeUp {
return
}
startRecognition(useBle: true, mode: "ble_wakeup") startRecognition(useBle: true, mode: "ble_wakeup")
} }

8
local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServicePlugin.swift

@ -194,6 +194,14 @@ public class AgentServicePlugin: NSObject, FlutterPlugin {
} }
let success = impl.setTtsRate(rate) let success = impl.setTtsRate(rate)
result(success) result(success)
case "setCanWakeUp":
guard let arguments = call.arguments as? [String: Any],
let canWakeUp = arguments["canWakeUp"] as? Bool else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "canWakeUp参数不能为空", details: nil))
return
}
impl.setCanWakeUp(canWakeUp)
result(true)
default: default:
result(FlutterMethodNotImplemented) result(FlutterMethodNotImplemented)
} }

8
local_plugins/agent_service/lib/agent_service.dart

@ -520,7 +520,10 @@ class AgentService {
} }
} }
/// 设置是否可以唤醒 /// 设置是否可以ble唤醒agent
///
/// [canWakeUp] 是否可以ble唤醒agent
/// 返回是否设置成功
static Future<bool> setCanWakeUp(bool canWakeUp) async { static Future<bool> setCanWakeUp(bool canWakeUp) async {
try { try {
final bool result = await _channel.invokeMethod('setCanWakeUp', { final bool result = await _channel.invokeMethod('setCanWakeUp', {
@ -528,7 +531,8 @@ class AgentService {
}); });
return result; return result;
} on PlatformException catch (e) { } on PlatformException catch (e) {
throw AgentServiceException(e.code, e.message ?? '设置是否可以唤醒失败', e.details); throw AgentServiceException(
e.code, e.message ?? '设置是否可以ble唤醒agent失败', e.details);
} }
} }

Loading…
Cancel
Save