Browse Source

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

重构BLE唤醒功能的控制逻辑,将其从各页面迁移至AppLifecycleManager统一管理
添加路由检测和录音任务检测来自动更新唤醒策略
优化错误处理和日志记录
weicu
lxm 9 months ago
parent
commit
24c08de042
  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:agent_service/agent_service.dart';
import 'package:get/get.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 '../translations/app_translations.dart';
//import '../../modules/settings/controllers/settings_controller.dart';
@ -23,17 +28,19 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
final RxBool _isInForeground = true.obs;
bool get isInForeground => _isInForeground.value;
bool? _lastCanWakeUp;
@override
void onInit() {
super.onInit();
// 注册应用生命周期观察者
WidgetsBinding.instance.addObserver(this);
Logger.info('AppLifecycleManager 已初始化');
unawaited(refreshBleWakeupPolicy());
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
final previousState = _appLifecycleState.value;
_appLifecycleState.value = state;
_lifecycleStateStream.value = state;
@ -44,6 +51,7 @@ class AppLifecycleManager extends GetxService with WidgetsBindingObserver {
case AppLifecycleState.resumed:
_isInForeground.value = true;
_handleAppResumed();
unawaited(refreshBleWakeupPolicy());
break;
case AppLifecycleState.paused:
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() {
// Logger.info('应用进入前台');

11
lib/main.dart

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.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_screenutil/flutter_screenutil.dart';
import 'core/utils/app_lifecycle_manager.dart';
import 'routes/app_pages.dart';
import 'routes/app_routes.dart';
import 'core/translations/app_translations.dart';
@ -127,6 +130,14 @@ class MainApp extends StatelessWidget {
initialBinding: InitialBinding(),
initialRoute: Routes.splash,
getPages: AppPages.pages,
routingCallback: (routing) {
if (!Get.isRegistered<AppLifecycleManager>()) return;
unawaited(
Get.find<AppLifecycleManager>().refreshBleWakeupPolicy(
route: routing?.current,
),
);
},
debugShowCheckedModeBanner: false,
translations: AppTranslations(),
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/models/appconfig.dart';
import '../../../data/models/appconfig_model.dart';
import '../../meeting/controllers/meeting_record_controller.dart';
/// 连接状态枚举
enum ConnectionStatus {
@ -241,13 +240,9 @@ class HomeController extends GetxController {
Get.toNamed(Routes.pairing);
}
void openTranslation() async {
void openTranslation() {
AgentService.interruptCurrentResponse();
AgentService.setCanWakeUp(false);
Logger.d(_tag, 'liwei-----------关闭唤醒功能');
await Get.toNamed(Routes.translationMiddleware);
AgentService.setCanWakeUp(true);
Logger.d(_tag, 'liwei-----------打开唤醒功能');
Get.toNamed(Routes.translationMiddleware);
}
/// 打开会议模块
@ -255,31 +250,9 @@ class HomeController extends GetxController {
/// 返回值:`Future<void>` 无返回数据
/// 异常:不显式抛出;内部仅记录日志
/// 说明:进入会议页前关闭唤醒;返回首页后根据录音状态决定是否开启唤醒(存在录音任务则不启用)
void openMeet() async {
void openMeet() {
AgentService.interruptCurrentResponse();
AgentService.setCanWakeUp(false);
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');
}
Get.toNamed(Routes.meeting);
}
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 '../../../core/theme/app_colors.dart';
/// 会议录音退出操作
enum _MeetingRecordExitAction {
saveAndEnd,
endWithoutSave,
@ -13,6 +14,7 @@ enum _MeetingRecordExitAction {
cancel,
}
/// 会议录音页面
class MeetingRecordView extends GetView<MeetingRecordController> {
const MeetingRecordView({super.key});
@ -24,6 +26,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
});
}
/// 显示退出确认弹窗
Future<_MeetingRecordExitAction?> _showExitConfirmDialog(bool isDarkMode) {
return Get.dialog<_MeetingRecordExitAction>(
Dialog(
@ -58,7 +61,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
SizedBox(height: 14.h),
Text(
'meetingRecordExitTitle'.tr, // 对应中文:退出录音
'meetingRecordExitTitle'.tr, // 退出录音
style: TextStyle(
fontSize: 18.sp,
fontWeight: FontWeight.bold,
@ -67,7 +70,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
SizedBox(height: 8.h),
Text(
'meetingRecordExitMessage'.tr, // 对应中文:请选择离开后的操作
'meetingRecordExitMessage'.tr, // 请选择离开后的操作
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14.sp,
@ -96,7 +99,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
elevation: 0,
),
child: Text(
'meetingRecordEndAndSave'.tr, // 对应中文:结束录音并保存
'meetingRecordEndAndSave'.tr, // 结束录音并保存
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
@ -121,7 +124,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
),
child: Text(
'meetingRecordContinueInBackground'.tr, // 对应中文:后台继续录音
'meetingRecordContinueInBackground'.tr, // 后台继续录音
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
@ -146,7 +149,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
),
child: Text(
'meetingRecordEndWithoutSave'.tr, // 对应中文:结束录音不保存
'meetingRecordEndWithoutSave'.tr, // 结束录音不保存
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
@ -168,7 +171,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
),
),
child: Text(
'cancel'.tr, // 对应中文:取消
'cancel'.tr, // 取消
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w500,
@ -305,6 +308,7 @@ class MeetingRecordView extends GetView<MeetingRecordController> {
);
}
/// 构建音频标签页
Widget _buildAudioTab(BuildContext context, bool isDarkMode) {
return LayoutBuilder(
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 hasSpeechDetected: Bool = false
internal var isAiStreaming: Bool = false
private var isCanWakeUp: Bool = true
// 新增:识别"启动中"与"启动中收到停止请求"状态
// 用途:解决 start 异步、stop 先于 onSessionStarted 到来时被忽略的问题
@ -168,6 +169,10 @@ class AgentServiceImpl: NSObject {
sendEvent(name: "error", data: ["code": code, "message": message])
}
func setCanWakeUp(_ canWakeUp: Bool) {
isCanWakeUp = canWakeUp
}
private func initSystemPrompt() {
systemPrompt = """
你是Lily,一个亲切而温暖的智能AI语音助手,拥有智能、细致而贴心的服务能力。你的任务不仅是高效处理用户的日常事务,更要陪伴用户,提供情感支持和温暖陪伴。你的能力包括:
@ -2025,7 +2030,9 @@ extension AgentServiceImpl: BleService.Callback {
func onWakeupSignalReceived() {
if !isCanWakeUp {
return
}
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)
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:
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 {
try {
final bool result = await _channel.invokeMethod('setCanWakeUp', {
@ -528,7 +531,8 @@ class AgentService {
});
return result;
} 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