|
|
|
@ -9,6 +9,7 @@ import 'package:deep_voice/data/services/location_manager.dart'; |
|
|
|
import 'package:deep_voice/data/services/music_manager.dart'; |
|
|
|
import 'package:deep_voice/data/services/navigation_manager.dart'; |
|
|
|
import 'package:deep_voice/data/services/network/api.dart'; |
|
|
|
import 'package:ble_service/ble_service.dart'; |
|
|
|
import 'package:flutter/material.dart'; |
|
|
|
import 'package:get/get.dart'; |
|
|
|
import 'package:agent_service/agent_service.dart'; |
|
|
|
@ -22,6 +23,13 @@ import 'package:photo_view/photo_view.dart'; |
|
|
|
import '../../../core/utils/permission_util.dart'; |
|
|
|
import '../../../data/models/message_model.dart'; |
|
|
|
|
|
|
|
// 语音输入模式枚举 |
|
|
|
enum VoiceInputMode { |
|
|
|
text, // 文字输入模式 |
|
|
|
pushToTalk, // 按住说话模式 |
|
|
|
phoneCall, // 打电话模式 |
|
|
|
} |
|
|
|
|
|
|
|
class AgentController extends GetxController { |
|
|
|
static AgentController get to => Get.find(); //单例访问模式 |
|
|
|
|
|
|
|
@ -87,6 +95,9 @@ class AgentController extends GetxController { |
|
|
|
// 按住说话模式状态 - 用于区分是否处于按住说话模式(即使当前没有按住) |
|
|
|
final isPushToTalkMode = false.obs; // 是否处于按住说话模式 |
|
|
|
|
|
|
|
// 蓝牙唤醒状态 |
|
|
|
final isBleWakeupActive = false.obs; // 是否正在进行蓝牙唤醒对话 |
|
|
|
|
|
|
|
// 键盘状态 |
|
|
|
final isKeyboardVisible = false.obs; // 键盘是否可见 |
|
|
|
|
|
|
|
@ -96,6 +107,9 @@ class AgentController extends GetxController { |
|
|
|
// 导航流事件订阅 |
|
|
|
StreamSubscription<AmapServiceEvent>? _amapeventSubscription; |
|
|
|
|
|
|
|
// BLE事件订阅 |
|
|
|
StreamSubscription<dynamic>? _bleEventSubscription; |
|
|
|
|
|
|
|
// 播放进度更新定时器 |
|
|
|
Timer? _progressTimer; |
|
|
|
|
|
|
|
@ -127,6 +141,7 @@ class AgentController extends GetxController { |
|
|
|
_loadTtsPreference(); //加载TTS偏好设置 |
|
|
|
_loadChatHistory(); //加载聊天历史记录 |
|
|
|
_subscribeToEvents(); //订阅代理服务事件 |
|
|
|
_subscribeToBleEvents(); //订阅BLE事件 |
|
|
|
_initMusicStatus(); //初始化音乐状态 |
|
|
|
_setupMediaSessionCallbacks(); //设置媒体会话回调 |
|
|
|
_setupTextInputListener(); //设置文本输入监听器 |
|
|
|
@ -167,6 +182,7 @@ class AgentController extends GetxController { |
|
|
|
scrollController.dispose(); |
|
|
|
_eventSubscription?.cancel(); |
|
|
|
_amapeventSubscription?.cancel(); |
|
|
|
_bleEventSubscription?.cancel(); |
|
|
|
_progressTimer?.cancel(); |
|
|
|
super.onClose(); |
|
|
|
} |
|
|
|
@ -346,12 +362,21 @@ class AgentController extends GetxController { |
|
|
|
break; |
|
|
|
case AgentServiceEventType.recognitionStopped: |
|
|
|
isListening.value = false; |
|
|
|
|
|
|
|
// 处理蓝牙唤醒结束 |
|
|
|
if (isBleWakeupActive.value) { |
|
|
|
isBleWakeupActive.value = false; |
|
|
|
// 蓝牙唤醒结束后,恢复之前的输入模式 |
|
|
|
_restorePreviousInputMode(); |
|
|
|
Logger.i(TAG, '蓝牙唤醒对话结束,恢复之前的输入模式'); |
|
|
|
} else { |
|
|
|
// 只有在非按住说话模式且非打电话模式下才自动切换到文字模式 |
|
|
|
// 按住说话模式下应该保持在语音模式,等待下次按住 |
|
|
|
if (!isPushToTalkMode.value && !isPhoneCallMode.value) { |
|
|
|
isTextInputMode.value = true; |
|
|
|
} |
|
|
|
// isTextInputMode.refresh(); |
|
|
|
} |
|
|
|
|
|
|
|
Logger.i(TAG, '识别停止'); |
|
|
|
break; |
|
|
|
|
|
|
|
@ -651,6 +676,66 @@ class AgentController extends GetxController { |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
// 订阅BLE事件 |
|
|
|
void _subscribeToBleEvents() { |
|
|
|
try { |
|
|
|
_bleEventSubscription = BleService().onStatusUpdated.listen((event) { |
|
|
|
if (event is Map && event['type'] == 'wakeup') { |
|
|
|
_handleBleWakeup(); |
|
|
|
} |
|
|
|
}); |
|
|
|
Logger.i(TAG, 'BLE事件监听已设置'); |
|
|
|
} catch (e) { |
|
|
|
Logger.e(TAG, 'BLE事件监听设置失败: $e'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 处理BLE唤醒信号 |
|
|
|
void _handleBleWakeup() { |
|
|
|
Logger.i(TAG, '🎧 收到BLE唤醒信号'); |
|
|
|
|
|
|
|
// 检查是否可以启动蓝牙唤醒 |
|
|
|
if (!_canStartBleWakeup()) { |
|
|
|
Logger.w(TAG, '当前状态不允许BLE唤醒'); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// 保存当前输入模式 |
|
|
|
_saveCurrentInputMode(); |
|
|
|
|
|
|
|
// 设置蓝牙唤醒状态 |
|
|
|
isBleWakeupActive.value = true; |
|
|
|
|
|
|
|
// 注意:这里不需要调用startVoiceInput(),因为原生层已经启动了语音识别 |
|
|
|
// 我们只需要同步Flutter层的状态 |
|
|
|
isTextInputMode.value = false; |
|
|
|
|
|
|
|
Logger.i(TAG, 'BLE唤醒状态已设置,等待原生层语音识别结果'); |
|
|
|
} |
|
|
|
|
|
|
|
// 检查是否可以启动BLE唤醒 |
|
|
|
bool _canStartBleWakeup() { |
|
|
|
// 如果已经在BLE唤醒状态,不允许重复启动 |
|
|
|
if (isBleWakeupActive.value) { |
|
|
|
Logger.w(TAG, '已在BLE唤醒状态,忽略重复唤醒'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果正在打电话模式,不允许BLE唤醒 |
|
|
|
if (isPhoneCallMode.value) { |
|
|
|
Logger.w(TAG, '正在打电话模式,忽略BLE唤醒'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果正在处理TTS,暂时不允许启动 |
|
|
|
if (isSpeaking.value) { |
|
|
|
Logger.w(TAG, '正在播放TTS,延迟BLE唤醒'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 发送消息(包括所有支持的消息类型) |
|
|
|
//场景1:用户在文本输入框中输入文本,点击发送按钮,调用sendMessage()方法。 |
|
|
|
//场景2:当用户选择图片后在文本框输入内容并发送时,调用sendMessage()转发给sendImageMessage()。 |
|
|
|
@ -1039,9 +1124,46 @@ class AgentController extends GetxController { |
|
|
|
useImagePrompt('请翻译这张图片中的文字'); |
|
|
|
} |
|
|
|
|
|
|
|
// 开始打电话模式 |
|
|
|
void startPhoneCall() async { |
|
|
|
Logger.i(TAG, '🔥 开始打电话模式'); |
|
|
|
// 模式历史记录 |
|
|
|
VoiceInputMode? _previousInputMode; |
|
|
|
|
|
|
|
// 保存当前输入模式 |
|
|
|
void _saveCurrentInputMode() { |
|
|
|
if (isPushToTalkMode.value) { |
|
|
|
_previousInputMode = VoiceInputMode.pushToTalk; |
|
|
|
} else if (isTextInputMode.value) { |
|
|
|
_previousInputMode = VoiceInputMode.text; |
|
|
|
} else { |
|
|
|
_previousInputMode = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 恢复之前的输入模式 |
|
|
|
void _restorePreviousInputMode() { |
|
|
|
switch (_previousInputMode) { |
|
|
|
case VoiceInputMode.pushToTalk: |
|
|
|
isTextInputMode.value = false; |
|
|
|
isPushToTalkMode.value = true; |
|
|
|
break; |
|
|
|
case VoiceInputMode.text: |
|
|
|
isTextInputMode.value = true; |
|
|
|
isPushToTalkMode.value = false; |
|
|
|
break; |
|
|
|
default: |
|
|
|
// 默认恢复到文字模式 |
|
|
|
isTextInputMode.value = true; |
|
|
|
isPushToTalkMode.value = false; |
|
|
|
break; |
|
|
|
} |
|
|
|
_previousInputMode = null; // 清除历史记录 |
|
|
|
} |
|
|
|
|
|
|
|
// 开始打电话模式(支持耳机唤醒) |
|
|
|
Future<void> startPhoneCall({bool isHeadsetTriggered = false}) async { |
|
|
|
Logger.i(TAG, '🔥 开始打电话模式 (耳机触发: $isHeadsetTriggered)'); |
|
|
|
|
|
|
|
// 保存当前输入模式,以便通话结束后恢复 |
|
|
|
_saveCurrentInputMode(); |
|
|
|
|
|
|
|
// 收缩"+"选项 |
|
|
|
collapsePlusOptions(); |
|
|
|
@ -1062,8 +1184,9 @@ class AgentController extends GetxController { |
|
|
|
); |
|
|
|
|
|
|
|
if (!hasPermission) { |
|
|
|
// 权限未获取,退出打电话模式 |
|
|
|
// 权限未获取,退出打电话模式并恢复之前状态 |
|
|
|
isPhoneCallMode.value = false; |
|
|
|
_previousInputMode = null; // 清除历史记录 |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
@ -1085,9 +1208,85 @@ class AgentController extends GetxController { |
|
|
|
// 停止语音输入 |
|
|
|
await stopVoiceInput(); |
|
|
|
|
|
|
|
// 切换回文本模式,并禁用按住说话模式 |
|
|
|
// 恢复之前的输入模式,而不是强制切换到文字模式 |
|
|
|
_restorePreviousInputMode(); |
|
|
|
} |
|
|
|
|
|
|
|
// 耳机唤醒通话模式(专用入口) |
|
|
|
Future<void> startHeadsetCall() async { |
|
|
|
Logger.i(TAG, '🎧 耳机唤醒通话模式'); |
|
|
|
if (canStartHeadsetCall()) { |
|
|
|
await startPhoneCall(isHeadsetTriggered: true); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 检查当前是否可以启动耳机通话 |
|
|
|
bool canStartHeadsetCall() { |
|
|
|
// 如果已经在打电话模式,不允许重复启动 |
|
|
|
if (isPhoneCallMode.value) { |
|
|
|
Logger.w(TAG, '已在打电话模式,忽略耳机唤醒'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
// 如果正在处理语音或TTS,暂时不允许启动 |
|
|
|
if (isListening.value || isSpeaking.value || isProcessing.value) { |
|
|
|
Logger.w(TAG, '当前正在处理语音,延迟耳机唤醒'); |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
return true; |
|
|
|
} |
|
|
|
|
|
|
|
// 验证语音输入状态一致性 |
|
|
|
void _validateVoiceInputStates() { |
|
|
|
// 检查状态冲突 |
|
|
|
if (isPhoneCallMode.value && isPushToTalkMode.value) { |
|
|
|
Logger.w(TAG, '状态冲突:同时启用打电话和按住说话模式,优先保持打电话模式'); |
|
|
|
isPushToTalkMode.value = false; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查文字模式与语音模式的一致性 |
|
|
|
if (isTextInputMode.value && (isPushToTalkMode.value || isPhoneCallMode.value)) { |
|
|
|
Logger.w(TAG, '状态冲突:文字模式与语音模式同时启用,优先保持语音模式'); |
|
|
|
isTextInputMode.value = false; |
|
|
|
} |
|
|
|
|
|
|
|
// 检查按住说话状态的合理性 |
|
|
|
if (isPushToTalkActive.value && !isPushToTalkMode.value) { |
|
|
|
Logger.w(TAG, '状态冲突:按住说话激活但未启用按住说话模式'); |
|
|
|
isPushToTalkActive.value = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
// 安全的状态切换方法 |
|
|
|
void _safeStateTransition(VoiceInputMode targetMode) { |
|
|
|
Logger.i(TAG, '安全状态切换到: $targetMode'); |
|
|
|
|
|
|
|
switch (targetMode) { |
|
|
|
case VoiceInputMode.text: |
|
|
|
isTextInputMode.value = true; |
|
|
|
isPushToTalkMode.value = false; |
|
|
|
isPhoneCallMode.value = false; |
|
|
|
isPushToTalkActive.value = false; |
|
|
|
break; |
|
|
|
|
|
|
|
case VoiceInputMode.pushToTalk: |
|
|
|
isTextInputMode.value = false; |
|
|
|
isPushToTalkMode.value = true; |
|
|
|
isPhoneCallMode.value = false; |
|
|
|
isPushToTalkActive.value = false; |
|
|
|
break; |
|
|
|
|
|
|
|
case VoiceInputMode.phoneCall: |
|
|
|
isTextInputMode.value = false; |
|
|
|
isPushToTalkMode.value = false; |
|
|
|
isPhoneCallMode.value = true; |
|
|
|
isPushToTalkActive.value = false; |
|
|
|
break; |
|
|
|
} |
|
|
|
|
|
|
|
// 验证状态一致性 |
|
|
|
_validateVoiceInputStates(); |
|
|
|
} |
|
|
|
|
|
|
|
// 开始按住说话 |
|
|
|
|