diff --git a/lib/data/models/appconfig.dart b/lib/data/models/appconfig.dart index 1c04272a6..e80862486 100644 --- a/lib/data/models/appconfig.dart +++ b/lib/data/models/appconfig.dart @@ -21,8 +21,8 @@ class AppConfig { } //智能体配置 - static String agent(String agentId) { + static String? agentsystemPrompt(String agentId) { final foundAgent = config.agents.firstWhere((agent) => agent.id == agentId); - return jsonEncode((foundAgent)); + return foundAgent.systemPrompt; } } diff --git a/lib/data/services/asr_service.dart b/lib/data/services/asr_service.dart index fee73821c..75c2457c6 100644 --- a/lib/data/services/asr_service.dart +++ b/lib/data/services/asr_service.dart @@ -19,7 +19,9 @@ abstract class AsrService { /// 开始连续语音识别 /// /// 返回一个包含识别事件的流 - Future> startContinuousRecognition(); + Future> startContinuousRecognition( + bool audioSourceType, + ); /// 停止连续语音识别 Future stopContinuousRecognition(); diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ce8302c92..cc72b46c6 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -404,6 +404,8 @@ class BleManager extends GetxService { final openaiBaseUrl = AppConfig.env('OPENAI_BASE_URL') ?? 'https://api.openai.com/v1'; final openaiModel = AppConfig.env('OPENAI_MODEL') ?? 'gpt-3.5-turbo'; + final systemPrompt = AppConfig.agentsystemPrompt("personal_assistant"); + // 从Flutter资源包中加载.mcp.json final jsonData = AppConfig.mcpConfig(); @@ -414,6 +416,7 @@ class BleManager extends GetxService { 'openaiApiKey': openaiApiKey, 'openaiBaseUrl': openaiBaseUrl, 'openaiModel': openaiModel, + 'systemPrompt': systemPrompt, 'mcpServer': jsonEncode(jsonData), }; @@ -483,9 +486,6 @@ class BleManager extends GetxService { } } - registerBackgroundScan() { - _bleService.registerBackgroundScan(); - } /// 获取电池电量 Future getBatteryLevel() async { @@ -537,6 +537,16 @@ class BleManager extends GetxService { } } + /// 打开解码器 + Future openA2DPDecoder() async { + try { + return await _bleService.openA2DPDecoder(); + } catch (e) { + Logger.error('打开解码器失败: $e'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { diff --git a/lib/data/services/music_manager.dart b/lib/data/services/music_manager.dart index 5efabeaf1..0732f0f6b 100644 --- a/lib/data/services/music_manager.dart +++ b/lib/data/services/music_manager.dart @@ -22,6 +22,23 @@ class MusicManager extends GetxService { super.onClose(); } - //播放 音乐 - Future play(String url) async {} + //播放 + Future play(String url) async { + await _musicService.playMusic(url); + } + + //暂停 + Future pause() async { + await _musicService.pauseMusic(); + } + + //继续 + Future resume() async { + await _musicService.resumeMusic(); + } + + //状态 + Future> getStatus() async { + return await _musicService.getStatus(); + } } diff --git a/lib/data/services/network/api.dart b/lib/data/services/network/api.dart index 5318d8d15..98daae281 100644 --- a/lib/data/services/network/api.dart +++ b/lib/data/services/network/api.dart @@ -73,4 +73,13 @@ class Api { params: params, ); } + + //app获取设备信息 + static getequipmentinfo([params]) { + return DioManager().request( + NWMethod.post, + '/api/home/user_getequipmentinfo', + params: params, + ); + } } diff --git a/lib/data/services/speech_impl/azure_asr_service.dart b/lib/data/services/speech_impl/azure_asr_service.dart index d15038a97..5c87577ee 100644 --- a/lib/data/services/speech_impl/azure_asr_service.dart +++ b/lib/data/services/speech_impl/azure_asr_service.dart @@ -150,7 +150,8 @@ class AzureAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } @@ -164,7 +165,9 @@ class AzureAsrService extends GetxService implements AsrService { // 开始连续识别 final bool result = - await _channel.invokeMethod('startContinuousRecognition'); + await _channel.invokeMethod('startContinuousRecognition', { + 'audioSourceType': audioSourceType, + }); if (!result) { _cleanupEventStream(); diff --git a/lib/data/services/speech_impl/flutter_asr_service.dart b/lib/data/services/speech_impl/flutter_asr_service.dart index d1e7fb6d8..438953af8 100644 --- a/lib/data/services/speech_impl/flutter_asr_service.dart +++ b/lib/data/services/speech_impl/flutter_asr_service.dart @@ -363,7 +363,8 @@ class FlutterAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { try { if (!_isAvailable.value) { final controller = StreamController.broadcast(); diff --git a/lib/data/services/speech_impl/volcano_asr_api_service.dart b/lib/data/services/speech_impl/volcano_asr_api_service.dart index bab3790ef..3ffc4e75e 100644 --- a/lib/data/services/speech_impl/volcano_asr_api_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_api_service.dart @@ -36,7 +36,8 @@ class VolcanoAsrApiService implements AsrService { final int _bitsPerSample = 16; final int _channels = 1; final String _encoding = 'pcm'; - + // 音频输入源 + bool _audioSourceType = false; // 连接相关 WebSocketChannel? _channel; Completer? _connectionCompleter; @@ -113,7 +114,7 @@ class VolcanoAsrApiService implements AsrService { final resultCompleter = Completer(); // 开始连续识别流程 - final stream = await startContinuousRecognition(); + final stream = await startContinuousRecognition(_audioSourceType); // 监听结果 StreamSubscription? subscription; @@ -156,7 +157,8 @@ class VolcanoAsrApiService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } diff --git a/lib/data/services/speech_impl/volcano_asr_service.dart b/lib/data/services/speech_impl/volcano_asr_service.dart index 636c2d834..ccb53f447 100644 --- a/lib/data/services/speech_impl/volcano_asr_service.dart +++ b/lib/data/services/speech_impl/volcano_asr_service.dart @@ -265,7 +265,8 @@ class VolcanoAsrService extends GetxService implements AsrService { /// 开始连续语音识别 @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!await _ensureInitialized()) { _eventStreamController.add(RecognitionEvent( type: RecognitionEventType.error, @@ -275,9 +276,10 @@ class VolcanoAsrService extends GetxService implements AsrService { } try { - final success = - await _channel.invokeMethod('startContinuousRecognition') ?? - false; + final success = await _channel.invokeMethod('audioSourceType', { + 'audioSourceType': audioSourceType, + }) ?? + false; if (success) { _isContinuousRecognitionActive = true; diff --git a/lib/data/services/speech_impl/xunfei_asr_service.dart b/lib/data/services/speech_impl/xunfei_asr_service.dart index 1201f7c66..ef6791e3f 100644 --- a/lib/data/services/speech_impl/xunfei_asr_service.dart +++ b/lib/data/services/speech_impl/xunfei_asr_service.dart @@ -75,7 +75,8 @@ class XunfeiAsrService extends GetxService implements AsrService { } @override - Future> startContinuousRecognition() async { + Future> startContinuousRecognition( + bool audioSourceType) async { if (!_isInitialized) { await initialize(); } diff --git a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart index e9c9e1410..46dbb2391 100644 --- a/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart +++ b/lib/data/services/voice_interaction_impl/ios_voice_interaction_service.dart @@ -9,8 +9,8 @@ import '../../models/events/voice_interaction_event.dart'; import '../../../core/utils/logger.dart'; enum VoiceInteractionState { - idle, // 等待唤醒 - active, // 活跃状态 - 可以同时识别和播放 + idle, // 等待唤醒 + active, // 活跃状态 - 可以同时识别和播放 } class IosVoiceInteractionService extends GetxService { @@ -18,45 +18,47 @@ class IosVoiceInteractionService extends GetxService { final TtsService _ttsService = Get.find(); final VolcanoAIService _aiService = Get.find(); final ChatHistoryService _chatHistoryService = Get.find(); - + // 当前状态 final Rx state = VoiceInteractionState.idle.obs; - + // 是否识别到用户语音 final RxBool isSpeechDetected = false.obs; - + // 是否正在处理AI响应 final RxBool isProcessingAI = false.obs; - + // 是否正在播放TTS final RxBool isSpeaking = false.obs; - + // 对话历史记录 final List> _messageHistory = []; - + // 最大历史记录数 static const int _maxHistorySize = 10; - + // 超时计时器 Timer? _inactivityTimer; - + // 流订阅 StreamSubscription? _recognitionSubscription; - + // 当前识别的文本 String _currentRecognizedText = ''; - + // 音频输入源 + bool _audioSourceType = false; // 事件流控制器 - final StreamController _eventStreamController = + final StreamController _eventStreamController = StreamController.broadcast(); - + // 获取事件流 @override - Stream get eventStream => _eventStreamController.stream; - + Stream get eventStream => + _eventStreamController.stream; + // 标识符,用于存储聊天历史 final String _agentId = 'ios_assistant'; - + /// 初始化服务 @override Future initialize() async { @@ -66,10 +68,10 @@ class IosVoiceInteractionService extends GetxService { } catch (e) { Logger.error('iOS语音交互服务初始化失败: $e'); } - + return this; } - + // 处理唤醒事件 void onWakeup() { _ttsService.speakOnce('我在呢!'); @@ -79,63 +81,61 @@ class IosVoiceInteractionService extends GetxService { _activateInteraction(); } } - + // 激活交互 void _activateInteraction() { // 设置状态为活跃 state.value = VoiceInteractionState.active; - + // 开始连续监听 _startContinuousListening(); - + // 启动不活动计时器 _startInactivityTimer(); - + // 发送识别开始事件 _sendRecognitionStartedEvent(); } - + // 开始连续监听 Future _startContinuousListening() async { // 停止之前的监听 _recognitionSubscription?.cancel(); - + try { // 开始连续识别 - final recognitionStream = await _asrService.startContinuousRecognition(); - + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); + // 监听识别结果 - _recognitionSubscription = recognitionStream.listen( - _handleRecognitionEvent, - onError: (error) { - Logger.error('语音识别错误: $error'); - _startContinuousListening(); // 尝试重新启动 - } - ); - + _recognitionSubscription = + recognitionStream.listen(_handleRecognitionEvent, onError: (error) { + Logger.error('语音识别错误: $error'); + _startContinuousListening(); // 尝试重新启动 + }); + Logger.info('开始连续语音识别'); } catch (e) { Logger.error('启动语音识别失败: $e'); - } } - + // 处理识别事件 void _handleRecognitionEvent(RecognitionEvent event) { // 更新活动时间 _updateActivityTime(); - + switch (event.type) { case RecognitionEventType.intermediateResult: // 检测用户是否开始说话 if (event.text.isNotEmpty && !isSpeechDetected.value) { isSpeechDetected.value = true; - + // 用户开始讲话时,立即中断当前响应 _interruptCurrentResponse("检测到用户开始讲话,中断当前响应"); } break; - + case RecognitionEventType.finalResult: if (event.text.isNotEmpty) { // 最终结果,处理用户输入 @@ -144,16 +144,16 @@ class IosVoiceInteractionService extends GetxService { // 重置语音检测状态 isSpeechDetected.value = false; break; - + case RecognitionEventType.sessionStarted: Logger.info('语音识别会话开始'); break; - + case RecognitionEventType.sessionStopped: Logger.info('语音识别会话结束'); break; - + case RecognitionEventType.error: case RecognitionEventType.canceled: Logger.error('语音识别错误: ${event.error}'); @@ -161,11 +161,11 @@ class IosVoiceInteractionService extends GetxService { break; } } - + // 中断当前响应 void _interruptCurrentResponse(String reason) { Logger.info(reason); - + // 如果正在活跃状态,需要中断当前操作 if (state.value == VoiceInteractionState.active) { // 停止TTS播放 @@ -174,40 +174,40 @@ class IosVoiceInteractionService extends GetxService { isProcessingAI.value = false; } } - + // 处理用户输入 Future _processUserInput(String text) async { Logger.info('处理用户输入: $text'); - + // 设置为处理状态 isProcessingAI.value = true; - + // 添加用户消息到历史记录 final userMessage = {'role': 'user', 'content': text}; _addToHistory(userMessage); - + // 创建用户消息对象 final userMsg = Message( role: 'user', content: text, timestamp: DateTime.now(), ); - + // AI响应处理标志 bool isProcessingCancelled = false; - + try { // 构建消息历史 final messageHistory = _buildMessageHistory(); - + // 调用AI服务获取响应 final responseStream = _aiService.sendMessageStream( messages: messageHistory, systemPrompt: "你是一个智能助手,请简明扼要地回答问题。", ); - + String aiResponse = ''; - + // 等待AI响应 await for (final chunk in responseStream) { // 检查是否被用户打断 @@ -217,49 +217,49 @@ class IosVoiceInteractionService extends GetxService { Logger.info('AI响应生成过程中被用户打断'); break; } - + // 如果状态已改变(可能由其他原因导致),停止处理 if (state.value != VoiceInteractionState.active) { isProcessingCancelled = true; break; } - + aiResponse += chunk; - + // 立即播放当前文本块,实现边生成边播放 _speakStreamResponse(chunk); } - + // 如果处理被取消,不继续后续操作 if (isProcessingCancelled) { isProcessingAI.value = false; return; } - + // 确保完整响应被处理 if (aiResponse.isNotEmpty) { // 将AI响应添加到历史记录 final assistantMessage = {'role': 'assistant', 'content': aiResponse}; _addToHistory(assistantMessage); - + // 创建助手消息对象 final assistantMsg = Message( role: 'assistant', content: aiResponse, timestamp: DateTime.now(), ); - + // 保存聊天历史 await _saveChatHistory(userMsg, assistantMsg); - + // 发送聊天历史事件 _sendChatHistoryEvent(userMsg.content, assistantMsg.content); - + // 刷新TTS流,确保所有文本都被播放 await _ttsService.flushStream(); isSpeaking.value = false; } - + // 处理完成 isProcessingAI.value = false; } catch (e) { @@ -268,59 +268,59 @@ class IosVoiceInteractionService extends GetxService { isProcessingAI.value = false; } } - + // 构建用于AI服务的消息历史 List> _buildMessageHistory() { return List>.from(_messageHistory); } - + // 添加消息到历史记录 void _addToHistory(Map message) { _messageHistory.add(message); - + // 限制历史记录大小 while (_messageHistory.length > _maxHistorySize) { _messageHistory.removeAt(0); } } - + // 播放流式响应 void _speakStreamResponse(String chunk) { if (chunk.isEmpty) return; - + // 更新播放状态 isSpeaking.value = true; - + // 使用流式TTS播放 _ttsService.speakStream(chunk); - + // 更新活动时间 _updateActivityTime(); } - + // 停止TTS void _stopTts() { _ttsService.stop(); isSpeaking.value = false; } - + // 更新最后活动时间 DateTime _lastActivityTime = DateTime.now(); void _updateActivityTime() { _lastActivityTime = DateTime.now(); } - + // 启动不活动计时器(长时间无交互会切换到空闲状态) void _startInactivityTimer() { _cancelInactivityTimer(); - + _inactivityTimer = Timer.periodic(Duration(seconds: 5), (timer) { // 计算空闲时间 final idleTime = DateTime.now().difference(_lastActivityTime).inSeconds; - + // 如果空闲超过30秒,且不在播放或检测到语音,切换到空闲 - if (idleTime > 8 && - !isSpeaking.value && + if (idleTime > 8 && + !isSpeaking.value && !isProcessingAI.value && !isSpeechDetected.value) { _resetToIdle(); @@ -328,37 +328,37 @@ class IosVoiceInteractionService extends GetxService { } }); } - + // 取消不活动计时器 void _cancelInactivityTimer() { _inactivityTimer?.cancel(); _inactivityTimer = null; } - + // 重置到空闲状态 void _resetToIdle() { _ttsService.speakOnce('没有听到声音,暂停对话,双击耳机唤醒!'); _cancelInactivityTimer(); - + // 停止TTS _stopTts(); - + // 停止语音识别 _recognitionSubscription?.cancel(); _asrService.stopContinuousRecognition(); - + // 重置状态 state.value = VoiceInteractionState.idle; isSpeechDetected.value = false; isProcessingAI.value = false; isSpeaking.value = false; - + // 清空历史记录 _messageHistory.clear(); - + Logger.info('语音交互已重置为空闲状态'); } - + // 暂停语音交互 @override void pauseVoiceInteraction() { @@ -366,53 +366,53 @@ class IosVoiceInteractionService extends GetxService { _stopTts(); _recognitionSubscription?.cancel(); _asrService.stopContinuousRecognition(); - + // 重置所有状态 state.value = VoiceInteractionState.idle; isSpeechDetected.value = false; isProcessingAI.value = false; isSpeaking.value = false; - + // 清空历史记录 _messageHistory.clear(); } } - + // 恢复语音交互 void resumeVoiceInteraction() { if (state.value == VoiceInteractionState.idle) { _activateInteraction(); } } - + // 手动停止交互 void stopInteraction() { _resetToIdle(); } - + // 获取当前状态 VoiceInteractionState getCurrentState() { return state.value; } - + // 是否处于活跃状态 @override bool isActive() { return state.value != VoiceInteractionState.idle; } - + // 获取当前对话历史 @override List> getMessageHistory() { return List>.from(_messageHistory); } - + // 获取当前服务ID @override String getServiceId() { return _agentId; } - + // 释放资源 @override void onClose() { @@ -422,25 +422,25 @@ class IosVoiceInteractionService extends GetxService { _eventStreamController.close(); super.onClose(); } - + // 保存聊天历史 Future _saveChatHistory(Message userMsg, Message assistantMsg) async { try { // 加载现有历史记录 final existingMessages = await _chatHistoryService.loadHistory(_agentId); - + // 添加新消息 existingMessages.addAll([userMsg, assistantMsg]); - + // 保存更新后的历史记录 await _chatHistoryService.saveHistory(_agentId, existingMessages); - + Logger.info('保存聊天历史记录成功'); } catch (e) { Logger.error('保存聊天历史记录失败: $e'); } } - + // 发送聊天历史事件 void _sendChatHistoryEvent(String userMessage, String assistantMessage) { final event = ChatHistoryEvent( @@ -449,19 +449,18 @@ class IosVoiceInteractionService extends GetxService { assistantMessage: assistantMessage, timestamp: DateTime.now().millisecondsSinceEpoch, ); - + _eventStreamController.add(event); Logger.info('已发送聊天历史事件'); } - + // 发送识别开始事件 void _sendRecognitionStartedEvent() { final event = RecognitionStartedEvent( timestamp: DateTime.now().millisecondsSinceEpoch, ); - + _eventStreamController.add(event); Logger.info('已发送识别开始事件'); } } - diff --git a/lib/modules/agent/controllers/agent_controller.dart b/lib/modules/agent/controllers/agent_controller.dart index 7e85a3fac..07f5532a9 100644 --- a/lib/modules/agent/controllers/agent_controller.dart +++ b/lib/modules/agent/controllers/agent_controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:deep_voice/data/services/music_manager.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:agent_service/agent_service.dart'; @@ -40,6 +41,11 @@ class AgentController extends GetxController { final TextEditingController textController = TextEditingController(); final ScrollController scrollController = ScrollController(); + //音乐播放管理 + final _musiceManager = Get.find(); + var isPlaying = false.obs; + var currentUrl = "".obs; + // 状态标志 final isInitialized = false.obs; final isListening = false.obs; @@ -101,13 +107,17 @@ class AgentController extends GetxController { if (result.messages.isNotEmpty) { // 转换并添加消息到列表 - final historyMessages = result.messages - .map((msg) => Message( - isUser: msg.sender == 'user', - text: msg.message, - timestamp: DateTime.parse(msg.timestamp), - )) - .toList(); + final historyMessages = result.messages.map((msg) { + final meta = msg.metadata != null && msg.metadata != "" + ? jsonDecode(msg.metadata!) + : null; // 如果为空,使用空对象 + return Message( + isUser: msg.sender == 'user', + text: msg.message, + meta: meta, + timestamp: DateTime.parse(msg.timestamp), + ); + }).toList(); // 消息按时间排序,最新的消息在底部 historyMessages.sort((a, b) => a.timestamp.compareTo(b.timestamp)); @@ -607,4 +617,30 @@ class AgentController extends GetxController { void translateImageText() { useImagePrompt('请翻译这张图片中的文字'); } + + // 播放音乐 + void musicStatus() async { + var status = await _musiceManager.getStatus(); + logger.i('获取当前音乐播放状态 $status'); + isPlaying.value = status['isPlaying']; + currentUrl.value = status['url']; + } + + // 播放音乐 + void musicPlay(String url) { + _musiceManager.play(url); + isPlaying.value = true; + currentUrl.value = url; + } + + // 播放音乐 + void musicPause() { + _musiceManager.pause(); + isPlaying.value = false; + } + + void musicResume() { + _musiceManager.resume(); + isPlaying.value = true; + } } diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index cc539bc18..fa0c81478 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -1,9 +1,13 @@ +import 'package:common_utils/common_utils.dart'; import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'dart:io'; +import '../controllers/agent_controller.dart'; + /// 消息气泡组件 -class MessageBubble extends StatelessWidget { +class MessageBubble extends GetView { final bool isUser; final String message; final DateTime timestamp; @@ -199,11 +203,28 @@ class MessageBubble extends StatelessWidget { //音乐卡片 return _buildMusicItem(context, musicName, musicUrl, musicSinger); } else if (mate!.keys.contains('card_musiclist')) { + final rawList = mate!['card_musiclist']['musics'] as List; + final playlist = + rawList.map((e) => Map.from(e)).toList(); + // final playlist = + // mate!['card_musiclist']['musics'] as List>; + //音乐播放列表 - return _buildMusicContent(context); + return _buildMusicContent(context, playlist); } else if (mate!.keys.contains('card_weather')) { + final city = mate!['card_weather']['city'] as String; + + final rawList = mate!['card_weather']['dayweather'] as List; + final daylist = + rawList.map((e) => Map.from(e)).toList(); + final now = daylist[0]; //天气 - return _buildWeatherContent(context); + return _buildWeatherContent( + context, + city, + now, + daylist, + ); } // if (message.contains('播放列表') || message.contains('音乐')) { @@ -250,11 +271,12 @@ class MessageBubble extends StatelessWidget { } /// 构建音乐内容卡片 - Widget _buildMusicContent(BuildContext context) { + Widget _buildMusicContent( + BuildContext context, List> playlist) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( + const Text( '以下是一些推荐的音乐:', style: TextStyle( color: Colors.black87, @@ -301,18 +323,22 @@ class MessageBubble extends StatelessWidget { ), const SizedBox(height: 10), - // 歌曲示例 - _buildMusicItem( - context, - '雨声与钢琴曲', - "", - '纯音乐', - ), - Divider(height: 1, thickness: 1, color: Colors.grey[100]), - _buildMusicItem(context, 'Rainy Day', "", 'Coldplay'), - Divider(height: 1, thickness: 1, color: Colors.grey[100]), - _buildMusicItem(context, '雨天的爵士咖啡馆', "", '爵士合集'), + // 动态生成音乐条目 + ...playlist.asMap().entries.map((entry) { + final index = entry.key; + final item = entry.value; + final name = item["name"] ?? "未知歌曲"; + final url = item["url"] ?? ""; + final singer = item["sgener"] ?? "未知歌手"; + return Column( + children: [ + _buildMusicItem(context, name, url, singer), + if (index != playlist.length - 1) + Divider(height: 1, thickness: 1, color: Colors.grey[100]), + ], + ); + }).toList(), const SizedBox(height: 8), Center( child: TextButton( @@ -507,6 +533,8 @@ class MessageBubble extends StatelessWidget { /// 构建音乐项目 Widget _buildMusicItem( BuildContext context, String name, String url, String singer) { + final controller = Get.find(); // 获取控制器 + controller.musicStatus(); //刷新状态 return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Row( @@ -550,14 +578,35 @@ class MessageBubble extends StatelessWidget { ], ), ), - IconButton( - onPressed: () {}, - icon: - Icon(Icons.play_circle, color: Theme.of(context).primaryColor), - padding: EdgeInsets.zero, - constraints: BoxConstraints(), - iconSize: 24, - ), + Obx(() { + final isThisPlaying = controller.isPlaying.value && + controller.currentUrl.value == url; + + return IconButton( + onPressed: () { + if (controller.isPlaying.value) { + if (controller.currentUrl.value == url) { + controller.musicPause(); + } else { + controller.musicPlay(url); + } + } else { + if (controller.currentUrl.value == url) { + controller.musicResume(); + } else { + controller.musicPlay(url); + } + } + }, + icon: Icon( + isThisPlaying ? Icons.pause_circle : Icons.play_circle, + color: Theme.of(context).primaryColor, + ), + padding: EdgeInsets.zero, + constraints: BoxConstraints(), + iconSize: 24, + ); + }), ], ), ); @@ -622,7 +671,8 @@ class MessageBubble extends StatelessWidget { } /// 构建天气内容卡片 - Widget _buildWeatherContent(BuildContext context) { + Widget _buildWeatherContent(BuildContext context, String city, + Map now, List> days) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -664,7 +714,7 @@ class MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '北京', + city, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -673,7 +723,7 @@ class MessageBubble extends StatelessWidget { ), SizedBox(height: 4), Text( - '晴朗', + now["weatherCondition"], style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), @@ -683,7 +733,7 @@ class MessageBubble extends StatelessWidget { Row( children: [ Text( - '25°', + "${now["temperature"]}°", style: TextStyle( fontSize: 36, fontWeight: FontWeight.bold, @@ -695,14 +745,14 @@ class MessageBubble extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '最高: 29°', + '最高: ${now["maxTemperature"]}°', style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), ), ), Text( - '最低: 18°', + '最低: ${now["minTemperature"]}°', style: TextStyle( fontSize: 12, color: Colors.white.withOpacity(0.9), @@ -736,15 +786,31 @@ class MessageBubble extends StatelessWidget { ), ), SizedBox(height: 8), + // Row( + // mainAxisAlignment: MainAxisAlignment.spaceBetween, + // children: [ + // _buildForecastDay('周一', Icons.wb_sunny, '28°'), + // _buildForecastDay('周二', Icons.cloud, '25°'), + // _buildForecastDay('周三', Icons.wb_cloudy, '26°'), + // _buildForecastDay('周四', Icons.grain, '24°'), + // _buildForecastDay('周五', Icons.wb_sunny, '27°'), + // ], + // ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildForecastDay('周一', Icons.wb_sunny, '28°'), - _buildForecastDay('周二', Icons.cloud, '25°'), - _buildForecastDay('周三', Icons.wb_cloudy, '26°'), - _buildForecastDay('周四', Icons.grain, '24°'), - _buildForecastDay('周五', Icons.wb_sunny, '27°'), - ], + children: List.generate( + days.length, + (index) { + String dateStr = days[index]['date']; + DateTime? date = DateUtil.getDateTime(dateStr); + String weekday = DateUtil.getWeekday(date); + return _buildForecastDay( + weekday, + Icons.wb_sunny, + days[index]['temperature'], + ); + }, + ), ), SizedBox(height: 16), Container( @@ -753,7 +819,7 @@ class MessageBubble extends StatelessWidget { color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), - child: Row( + child: const Row( children: [ Icon( Icons.info_outline, diff --git a/lib/modules/chat/controllers/chat_controller.dart b/lib/modules/chat/controllers/chat_controller.dart index 7955486b1..2f7fd5f70 100644 --- a/lib/modules/chat/controllers/chat_controller.dart +++ b/lib/modules/chat/controllers/chat_controller.dart @@ -78,6 +78,9 @@ class ChatController extends GetxController { // 取消标志 bool _shouldCancelAiResponse = false; + // 音频输入源 + bool _audioSourceType = false; + // 自动停止语音识别计时器 Timer? _autoStopTimer; @@ -184,7 +187,6 @@ class ChatController extends GetxController { // 自动停止计时器会在startVoiceRecognition中启动 } - _getLocation(); player.playerStateStream.listen((state) async { @@ -211,8 +213,6 @@ class ChatController extends GetxController { _amapFlutter.startLocation(); } - - @override void onClose() { Logger.info('ChatController onClose'); @@ -295,7 +295,6 @@ class ChatController extends GetxController { Future sendMessage(String text) async { if (text.trim().isEmpty) return; - // 中断当前响应 _interruptCurrentResponse("发送新消息,中断当前响应"); @@ -558,12 +557,11 @@ class ChatController extends GetxController { Timer(const Duration(milliseconds: 260), () async { if (isDebounce) return; - // 中断当前响应 _interruptCurrentResponse("发送新消息,中断当前响应"); try { final recognitionStream = - await _asrService.startContinuousRecognition(); + await _asrService.startContinuousRecognition(_audioSourceType); await Future.delayed(const Duration(milliseconds: 260)); isDebounce = true; recognizedText.value = ''; @@ -619,11 +617,11 @@ class ChatController extends GetxController { Future startVoiceRecognition() async { if (isVoiceRecognitionActive.value) return; - try { // 启动连续语音识别 // 注意:在UI更新后,使用模拟波形动画替代实际麦克风输入的波形数据 - final recognitionStream = await _asrService.startContinuousRecognition(); + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); isVoiceRecognitionActive.value = true; isListening.value = true; diff --git a/lib/modules/meeting/controllers/meeting_record_controller.dart b/lib/modules/meeting/controllers/meeting_record_controller.dart index 01f708c0e..a83e1c312 100644 --- a/lib/modules/meeting/controllers/meeting_record_controller.dart +++ b/lib/modules/meeting/controllers/meeting_record_controller.dart @@ -111,7 +111,8 @@ class MeetingRecordController extends GetxController //开始录音 Future _startRecorder() async { - final recognitionStream = await _asrService.startContinuousRecognition(); + final recognitionStream = + await _asrService.startContinuousRecognition(false); _recognitionSubscription = recognitionStream.listen( _handleRecognitionEvent, ); diff --git a/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart b/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart new file mode 100644 index 000000000..f97146011 --- /dev/null +++ b/lib/modules/ota_upgrade/bindings/ota_upgrade_binding.dart @@ -0,0 +1,9 @@ +import 'package:get/get.dart'; +import '../controllers/ota_upgrade_controller.dart'; + +class OtaUpgradeBinding extends Bindings { + @override + void dependencies() { + Get.lazyPut(() => OtaUpgradeController()); + } +} diff --git a/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart new file mode 100644 index 000000000..878a97be6 --- /dev/null +++ b/lib/modules/ota_upgrade/controllers/ota_upgrade_controller.dart @@ -0,0 +1,213 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'dart:async'; +import 'dart:convert'; +import 'package:get_storage/get_storage.dart'; +import '../../../data/services/network/api.dart'; +import '../ota.dart'; +import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class OtaUpgradeController extends GetxController { + final Dio _dio = Dio(); + final RxString latestVersion = ''.obs; + final RxString currentVersion = ''.obs; + String updatepackageaddress = ''; + final RxDouble downloadProgress = 0.0.obs; + final RxBool isUpdating = false.obs; + final isUpdateAvailable = true.obs; + + final RxDouble progress = 0.0.obs; + + @override + void onInit() { + super.onInit(); + _getCurrentVersion(); + } + + // 获取当前版本信息 + Future _getCurrentVersion() async { + //发送ble获取固件版本 + var firmwareVersion = "1.1.0"; + currentVersion.value = firmwareVersion; + //发送ble获取产品id + var id = "a2"; + //发送api请求 + final response = await Api.getequipmentinfo({ + "id": id, + }); + if (response['info'] != '') { + print('getLoginToken:进来了'); + // 根据API设计调整 + final resp = OtaConfigResp.fromJson(response); + + print('成功解析: ${resp.id}, ${resp.factory}'); + latestVersion.value = resp.systemversion!; + updatepackageaddress = resp.updatepackageaddress!; + if (_isNewVersionAvailable(resp.systemversion!)) { + //需要升级 + print('需要更新'); + isUpdateAvailable.value = false; + } else { + isUpdateAvailable.value = true; + //已经是最新版本更新ui,按钮隐藏 + } + } else { + throw const FormatException('API 响应缺少 "info" 字段'); + } + } + + // 版本比较方法优化 + bool _isNewVersionAvailable(String latest) { + try { + List currentParts = currentVersion.value + .split('.') + .map((e) => int.parse(e.trim())) + .toList(); + List latestParts = + latest.split('.').map((e) => int.parse(e.trim())).toList(); + + for (int i = 0; i < latestParts.length; i++) { + int current = (i < currentParts.length) ? currentParts[i] : 0; + if (latestParts[i] > current) return true; + if (latestParts[i] < current) return false; + } + return false; + } catch (e) { + print('版本比较错误: $e'); + return false; + } + } + + // 版本更新 + Future checkUpdate() async { + try { + if (latestVersion.value != "") + _showUpdateDialog(latestVersion.value); + else { + Get.snackbar('错误', '获取最新版本信息失败'); + } + } catch (e) { + Get.snackbar('错误', '获取版本信息失败'); + } + } + + // 显示更新对话框 + void _showUpdateDialog(String latest) { + Get.dialog( + AlertDialog( + title: Text('发现新版本 $latest'), + content: Text('是否立即更新?'), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text('取消'), + ), + TextButton( + onPressed: () { + Get.back(); + startDownload(); + }, + child: Text('更新'), + ), + ], + ), + ); + } + + // 开始下载 + Future startDownload() async { + isUpdating.value = true; + // 查找最后一个 '/' 的位置 + int lastIndex = updatepackageaddress.lastIndexOf('/'); + + // 截取最后一个 '/' 后面的字符串 + String fileName = updatepackageaddress.substring(lastIndex + 1); + String directoryName = "ota_upgrades"; + + final dio = Dio(BaseOptions( + connectTimeout: const Duration(seconds: 5), + receiveTimeout: Duration.zero, + sendTimeout: const Duration(seconds: 5), + headers: {'Accept-Encoding': 'gzip'}, + )); + + Get.snackbar('升级命令已发送', '开始下载...'); + + try { + final Directory? downloadsDir = await getDownloadsDirectory(); + if (downloadsDir == null) { + throw FileSystemException('无法访问下载目录'); + } + + // 创建子目录:Download/APP + final targetDir = Directory('${downloadsDir.path}/$directoryName'); + if (!await targetDir.exists()) { + await targetDir.create(recursive: true); + } + + final savePath = '${targetDir.path}/$fileName'; + print('文件保存路径: $savePath'); // 关键日志 + + final options = Options( + receiveTimeout: Duration.zero, + followRedirects: true, + maxRedirects: 5, + ); + + DateTime lastUpdate = DateTime.now(); + await dio.download( + updatepackageaddress, + savePath, + options: options, + onReceiveProgress: (received, total) { + final now = DateTime.now(); + if (now.difference(lastUpdate).inMilliseconds > 1000) { + lastUpdate = now; + String progressMsg = total != -1 + ? '${(received / total * 100).toStringAsFixed(1)}%' + : '已下载:${(received / 1024 / 1024).toStringAsFixed(1)}MB'; + Get.snackbar( + '升级进度', + progressMsg, + duration: const Duration(seconds: 1), + ); + } + }, + ); + + // 下载完成后验证文件存在性 + File file = File(savePath); + if (await file.exists()) { + Get.snackbar('下载完成', '文件已保存至: $savePath'); // 明确提示路径 + } else { + Get.snackbar('错误', '文件未找到,请检查写入权限'); + } + isUpdating.value = false; + //下载完成,开始升级 + _installApp(savePath); + } on DioException catch (e) { + Get.snackbar('下载失败', '错误: ${e.message}'); + } on FileSystemException catch (e) { + Get.snackbar('存储失败', '无法写入文件: ${e.message}'); + } catch (e) { + Get.snackbar('未知错误', e.toString()); + } + } + + // 开始升级 + void _installApp(String path) async { + // final File file = File(path); + // if (await file.exists()) { + // // OpenResult result = await OpenFile.open(path); + // // if (result.type != ResultType.done) { + // // Get.snackbar('错误', '安装失败: ${result.message}'); + // // } + // } + } +} diff --git a/lib/modules/ota_upgrade/ota.dart b/lib/modules/ota_upgrade/ota.dart new file mode 100644 index 000000000..2c35ff1ea --- /dev/null +++ b/lib/modules/ota_upgrade/ota.dart @@ -0,0 +1,54 @@ +class OtaConfigResp { + // 单例实例 + static OtaConfigResp? _instance; + + // 单例访问器(增加空安全保护) + static OtaConfigResp get instance { + if (_instance == null) { + throw Exception("User must be initialized first"); + } + return _instance!; + } + + // 最终属性(不可变) + final String? id; + final String? factory; + final String? equipmentname; + final String? systemversion; + final String? updatepackageaddress; + // 私有构造函数 + OtaConfigResp._({ + this.id, + this.factory, + this.equipmentname, + this.systemversion, + this.updatepackageaddress, + }); + + factory OtaConfigResp.fromJson(Map json) { + print("数据获取fronTokenJson${json}"); + // 修复1:安全获取嵌套的 user 字段 + final infoData = json['info'] as Map?; // 允许空值 + + // 更新单例实例 + return _instance = OtaConfigResp._( + id: (infoData?['id'] as String?) ?? '', + factory: (infoData?['factory'] as String?) ?? '', + equipmentname: (infoData?['equipmentname'] as String?) ?? '', + systemversion: (infoData?['systemversion'] as String?) ?? '', + updatepackageaddress: + (infoData?['updatepackageaddress'] as String?) ?? '', + ); + } + + // 完整的JSON序列化 + Map toJson() => { + 'user': { + 'id': instance.id, + 'factory': instance.factory, + 'equipmentname': instance.equipmentname, + 'systemversion': instance.systemversion, + 'updatepackageaddress': instance.updatepackageaddress, + } + }; +} diff --git a/lib/modules/ota_upgrade/views/ota_upgrade_view.dart b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart new file mode 100644 index 000000000..bc507d1e9 --- /dev/null +++ b/lib/modules/ota_upgrade/views/ota_upgrade_view.dart @@ -0,0 +1,109 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../controllers/ota_upgrade_controller.dart'; + +class OtaUpgradeView extends GetView { + const OtaUpgradeView({Key? key}) : super(key: key); + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + // 添加外层Center + child: SingleChildScrollView( + // 添加滚动防止小屏溢出 + child: Padding( + padding: EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, // 水平居中 + mainAxisSize: MainAxisSize.min, // 紧凑布局 + children: [ + // 设备图标 + CircleAvatar( + radius: 60, + backgroundColor: Colors.blue[100], + child: Icon( + Icons.phone_iphone, + size: 60, + color: Colors.blue, + ), + ), + SizedBox(height: 30), + + // 版本信息 + Column( + children: [ + Text( + '当前版本', + style: TextStyle( + fontSize: 18, + color: Colors.grey, + ), + ), + SizedBox(height: 8), + Text( + 'v${controller.currentVersion}', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + SizedBox(height: 40), + + // 更新按钮/进度条 + Obx(() { + if (controller.isUpdating.value) { + return Column( + children: [ + SizedBox( + width: 200, // 限制进度条宽度 + child: LinearProgressIndicator( + value: controller.downloadProgress.value, + backgroundColor: Colors.grey[200], + valueColor: + AlwaysStoppedAnimation(Colors.blue), + minHeight: 12, + borderRadius: BorderRadius.circular(10), + ), + ), + SizedBox(height: 20), + Text( + '${(controller.downloadProgress.value * 100).toStringAsFixed(1)}%', + style: TextStyle(color: Colors.grey), + ) + ], + ); + } else { + return ElevatedButton( + onPressed: controller.isUpdateAvailable.value + ? null + : controller.checkUpdate, + style: ElevatedButton.styleFrom( + backgroundColor: controller.isUpdateAvailable.value + ? Colors.grey + : Colors.blue, + foregroundColor: Colors.white, + padding: + EdgeInsets.symmetric(horizontal: 40, vertical: 15), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + ), + child: Text( + controller.isUpdateAvailable.value ? '已是最新版本' : '立即更新', + style: TextStyle(fontSize: 16), + ), + ); + } + }), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/pairing/controllers/pairing_controller.dart b/lib/modules/pairing/controllers/pairing_controller.dart index 99f9f55fc..4dedaefcb 100644 --- a/lib/modules/pairing/controllers/pairing_controller.dart +++ b/lib/modules/pairing/controllers/pairing_controller.dart @@ -9,6 +9,7 @@ import '../../../data/services/bluetooth_manager.dart'; import 'package:classic_bluetooth/classic_bluetooth.dart' show BluetoothDevice; import '../../../data/services/ble_manager.dart'; import 'package:agent_service/agent_service.dart'; +import 'package:ble_service/ble_service.dart'; /// 配对控制器,负责检查蓝牙权限、状态,并处理配对流程 class PairingController extends GetxController { static PairingController get to => Get.find(); @@ -30,12 +31,19 @@ class PairingController extends GetxController { // 是否正在跳转到主页 bool _isNavigatingToHome = false; + + // 是否正在执行Companion关联流程 + bool _isAssociating = false; // 当前连接设备信息 BluetoothDevice? get currentDevice => _bluetoothService.currentDevice; // 是否已连接设备 bool get isDeviceConnected => currentDevice != null; + + // 是否已完成Companion关联 + final _companionAssociated = false.obs; + bool get isCompanionAssociated => _companionAssociated.value; // 服务状态监听 Worker? _bluetoothStateWorker; @@ -43,6 +51,9 @@ class PairingController extends GetxController { // 蓝牙服务 final _bluetoothService = Get.find(); + + // BLE服务 + final _bleService = BleService(); @override void onInit() { @@ -60,8 +71,6 @@ class PairingController extends GetxController { /// 初始化流程 Future _initialize() async { try { - - isLoading.value = true; statusDescription.value = 'checkingPermissions'.tr; @@ -86,11 +95,10 @@ class PairingController extends GetxController { // 刷新已连接设备 statusDescription.value = 'searchingDevice'.tr; - - // 设备连接时启动BLE后台服务 - _startAgentService(); - await refreshConnectedDevices(); + + // 检查是否已存在Companion设备关联,但不要重复调用关联流程 + await _checkExistingCompanionAssociation(); } catch (e) { Logger.error('初始化配对控制器失败: $e'); statusDescription.value = 'initializationFailure'.tr; @@ -99,6 +107,26 @@ class PairingController extends GetxController { _updateStatusDescription(); } } + + /// 检查是否已存在Companion设备关联 + Future _checkExistingCompanionAssociation() async { + // 只在Android平台执行 + if (!Platform.isAndroid) return; + + try { + final isSupported = await _bleService.isCompanionSupported(); + if (!isSupported) return; + + final pairedMac = await _bleService.getPairedMacAddress(); + if (pairedMac != null) { + Logger.info('发现已关联的Companion设备: $pairedMac'); + _companionAssociated.value = true; + await _bleService.registerBackgroundScan(); + } + } catch (e) { + Logger.error('检查现有Companion关联失败: $e'); + } + } /// 检查并请求必要的蓝牙权限 Future _checkAndRequestPermissions() async { @@ -161,11 +189,27 @@ class PairingController extends GetxController { _deviceWorker = ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) { if (device != null && !_isNavigatingToHome) { Logger.info('配对页面发现设备已连接: ${device.name}'); - _autoNavigateToHome(); + + // 当经典蓝牙设备连接成功,尝试进行Companion关联 + _handleDeviceConnected(); } _updateStatusDescription(); }); } + + /// 处理设备连接成功事件 + Future _handleDeviceConnected() async { + if (!isDeviceConnected || _isNavigatingToHome) return; + + // 如果已经完成关联,直接跳转 + if (_companionAssociated.value) { + _autoNavigateToHome(); + return; + } + + // 启动关联流程 + _startCompanionAssociation(); + } /// 更新状态描述 void _updateStatusDescription() { @@ -180,8 +224,13 @@ class PairingController extends GetxController { } if (isDeviceConnected) { - // 有设备连接时,显示设备名称 - statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; + if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) { + statusDescription.value = '${'associatingDevice'.tr} ${currentDevice!.name}'; + } else if (isCompanionAssociated) { + statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; + } else { + statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}'; + } return; } @@ -210,7 +259,9 @@ class PairingController extends GetxController { // 检查连接状态 if (isDeviceConnected) { Logger.info('刷新发现设备已连接: ${currentDevice!.name}'); - _autoNavigateToHome(); + + // 当发现设备已连接,统一由_handleDeviceConnected处理 + _handleDeviceConnected(); } else { Logger.info('刷新未发现已连接设备'); } @@ -222,20 +273,103 @@ class PairingController extends GetxController { _updateStatusDescription(); } } + + /// 启动Companion设备关联流程 + Future _startCompanionAssociation() async { + // 如果不是Android平台,已经跳转到首页,已经完成关联或正在进行关联,直接返回 + if (!Platform.isAndroid || _isNavigatingToHome || _companionAssociated.value || _isAssociating) { + return; + } + + // 设置关联状态标志 + _isAssociating = true; + _updateStatusDescription(); + + try { + // 检查是否支持Companion Device功能 + final isSupported = await _bleService.isCompanionSupported(); + if (!isSupported) { + Logger.info('设备不支持Companion Device功能,跳过关联'); + _companionAssociated.value = true; // 标记为已完成 + _autoNavigateToHome(); + return; + } + + // 检查是否已有配对的MAC地址 + final pairedMac = await _bleService.getPairedMacAddress(); + if (pairedMac != null) { + // 已有关联的设备,检查是否与当前连接的设备一致 + if (currentDevice?.address == pairedMac) { + Logger.info('已关联的Companion设备与当前连接的设备一致'); + _companionAssociated.value = true; + await _bleService.registerBackgroundScan(); + _autoNavigateToHome(); + return; + } else { + // 不一致,清除旧关联 + Logger.info('已关联的Companion设备与当前连接的设备不一致,清除旧关联'); + await _bleService.clearAssociations(); + } + } + + // 开始关联流程 + if (currentDevice != null) { + Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}'); + + final success = await _bleService.startDeviceAssociation( + deviceNamePattern: currentDevice!.name, + macAddress: currentDevice!.address, + ); + + if (success) { + Logger.info('Companion设备关联成功'); + _companionAssociated.value = true; + + // 注册后台扫描,确保应用被杀死后仍能被唤醒 + await _bleService.registerBackgroundScan(macAddress: currentDevice!.address); + + // 启动BLE后台服务(可选) + await _startAgentService(); + + // 关联和后台扫描都成功后,再跳转 + _autoNavigateToHome(); + } else { + Logger.error('Companion设备关联失败'); + // 关联失败,但仍然可以跳转到主页(用户体验考虑) + _autoNavigateToHome(); + } + } + } catch (e) { + Logger.error('Companion设备关联过程发生错误: $e'); + // 出错时仍可以跳转主页 + _autoNavigateToHome(); + } finally { + // 无论成功失败,都重置关联状态标志 + _isAssociating = false; + _updateStatusDescription(); + } + } /// 自动跳转到主页 - void _autoNavigateToHome() { + Future _autoNavigateToHome() async { if (_isNavigatingToHome) return; // 如果没有连接,不进行跳转 if (!isDeviceConnected) return; + // 在Android平台上,如果支持Companion且仍在关联中,不跳转 + if (Platform.isAndroid) { + final isSupported = await _bleService.isCompanionSupported(); + if (isSupported && !isCompanionAssociated && _isAssociating) { + Logger.info('等待Companion设备关联完成后再跳转'); + return; + } + } + _isNavigatingToHome = true; Logger.info('已找到连接的设备 ${currentDevice!.name},自动跳转到主页'); - - Future.delayed(const Duration(milliseconds: 500), () { Get.offAllNamed(Routes.home); }); @@ -244,17 +378,8 @@ class PairingController extends GetxController { /// 启动BLE后台服务 Future _startAgentService() async { try { - // if (!isDeviceConnected) return; + if (!isDeviceConnected) return; - // 确保麦克风权限已被授予 - final microphoneStatus = await Permission.microphone.status; - if (!microphoneStatus.isGranted) { - Logger.warning('麦克风权限未授予,语音交互需要此权限'); - final result = await Permission.microphone.request(); - if (!result.isGranted) { - Logger.error('麦克风权限被拒绝,语音功能将无法正常工作'); - } - } // 启动BLE服务,无需传递配置参数,将从.env读取 final result = await BleManager.to.startAgentService(); diff --git a/lib/modules/settings/views/settings_view.dart b/lib/modules/settings/views/settings_view.dart index 5a12ef143..eb7ea7512 100644 --- a/lib/modules/settings/views/settings_view.dart +++ b/lib/modules/settings/views/settings_view.dart @@ -555,11 +555,32 @@ class SettingsView extends GetView { title: '语音测试', subtitle: '测试语音识别和合成功能,支持多种服务实现', icon: Icons.record_voice_over, - iconBgColor: isDarkMode ? Colors.orange[900]!.withOpacity(0.3) : Colors.orange[100]!, - iconColor: isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: + isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, onTap: () => Get.toNamed(Routes.speechTest), isDarkMode: isDarkMode, ), + Divider( + height: 1, + color: isDarkMode + ? Colors.white.withOpacity(0.1) + : Colors.grey[200]), + // 测试ble耳机固件版本升级 + _buildSimpleNavigationSetting( + title: 'ota升级', + subtitle: '测试ble耳机固件版本升级', + icon: Icons.record_voice_over, + iconBgColor: isDarkMode + ? Colors.orange[900]!.withOpacity(0.3) + : Colors.orange[100]!, + iconColor: + isDarkMode ? Colors.orange[300]! : Colors.orange[600]!, + onTap: () => Get.toNamed(Routes.OTA_UPGRADE), + isDarkMode: isDarkMode, + ), Divider( height: 1, color: isDarkMode @@ -1191,8 +1212,7 @@ class SettingsView extends GetView { Obx(() { final serviceType = controller.speechServiceType.value; final serviceName = controller.availableSpeechServices - .firstWhere( - (service) => service['type'] == serviceType, + .firstWhere((service) => service['type'] == serviceType, orElse: () => {'name': 'unknown'})['name']; return Container( constraints: BoxConstraints(maxWidth: 120.w), // 限制最大宽度 diff --git a/lib/modules/speech_test/controllers/speech_test_controller.dart b/lib/modules/speech_test/controllers/speech_test_controller.dart index 9274f369f..4a9d195f1 100644 --- a/lib/modules/speech_test/controllers/speech_test_controller.dart +++ b/lib/modules/speech_test/controllers/speech_test_controller.dart @@ -11,47 +11,48 @@ import '../../../core/utils/logger.dart'; class SpeechTestController extends GetxController { // 语音服务工厂 final SpeechFactory _speechFactory = Get.find(); - + // 当前使用的TTS服务 late TtsService _ttsService; - + // 当前使用的ASR服务 late AsrService _asrService; - + // 状态变量 final RxBool isInitialized = false.obs; final RxBool isProcessing = false.obs; final RxBool isListening = false.obs; final RxBool isSpeaking = false.obs; - + // 识别结果 final RxString intermediateResult = ''.obs; // 中间结果 - final RxString finalResult = ''.obs; // 最终结果 + final RxString finalResult = ''.obs; // 最终结果 final RxString recognitionStatus = '准备就绪'.obs; - final RxString detectedLanguage = ''.obs; // 检测到的语言 - + final RxString detectedLanguage = ''.obs; // 检测到的语言 + // TTS事件 final RxString ttsStatus = '准备就绪'.obs; - + // 当前选择的语音服务类型 final Rx selectedServiceType = SpeechServiceType.azure.obs; - + // 音频输入源 + bool _audioSourceType = false; // 连续识别结果流 StreamSubscription? _recognitionSubscription; - + // TTS事件流 StreamSubscription? _ttsEventSubscription; - + // 文本控制器 final TextEditingController textController = TextEditingController(); - + // 连续播放的测试句子 final List testSentences = [ '这是第一句测试语音。', '这是第二句测试语音,语音合成支持连续播放。', '这是第三句测试语音,将在连续模式下播放完毕。' ]; - + @override void onInit() { super.onInit(); @@ -59,23 +60,23 @@ class SpeechTestController extends GetxController { textController.text = '欢迎使用语音测试,这是一段默认的测试文本。您可以修改此文本,然后点击播放按钮测试语音合成效果。'; _initServices(); } - + /// 初始化服务 Future _initServices() async { try { isProcessing.value = true; - + // 获取服务实例 _ttsService = _speechFactory.getTtsService(); _asrService = _speechFactory.getAsrService(); - + // 初始化服务 await _ttsService.initialize(); await _asrService.initialize(); - + // 监听TTS事件 _setupTtsEventListener(); - + isInitialized.value = true; ttsStatus.value = '准备就绪'; recognitionStatus.value = '准备就绪'; @@ -87,12 +88,12 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 设置TTS事件监听 void _setupTtsEventListener() { // 取消旧的订阅 _ttsEventSubscription?.cancel(); - + // 设置新的订阅 _ttsEventSubscription = _ttsService.onEvent.listen((event) { switch (event.type) { @@ -115,18 +116,18 @@ class SpeechTestController extends GetxController { } }); } - + /// 设置ASR事件监听 void _setupAsrEventListener(Stream stream) { // 取消旧的订阅 _recognitionSubscription?.cancel(); - + // 重置识别结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; recognitionStatus.value = '正在等待语音输入...'; - + // 设置新的订阅 _recognitionSubscription = stream.listen((event) { switch (event.type) { @@ -134,14 +135,14 @@ class SpeechTestController extends GetxController { isListening.value = true; recognitionStatus.value = '开始聆听...'; break; - + case RecognitionEventType.sessionStopped: isListening.value = false; recognitionStatus.value = '聆听已结束'; // 清空中间结果,保留最终结果 intermediateResult.value = ''; break; - + case RecognitionEventType.intermediateResult: // 更新中间结果,显示为斜体或不同颜色 intermediateResult.value = event.text; @@ -150,7 +151,7 @@ class SpeechTestController extends GetxController { } recognitionStatus.value = '正在识别...'; break; - + case RecognitionEventType.finalResult: // 更新最终结果,显示为正常文本 finalResult.value = event.text; @@ -160,12 +161,12 @@ class SpeechTestController extends GetxController { } recognitionStatus.value = '识别完成'; break; - + case RecognitionEventType.error: isListening.value = false; recognitionStatus.value = '识别错误: ${event.error}'; break; - + case RecognitionEventType.canceled: isListening.value = false; recognitionStatus.value = '识别已取消'; @@ -179,21 +180,21 @@ class SpeechTestController extends GetxController { intermediateResult.value = ''; }); } - + /// 切换语音服务类型 void switchServiceType(SpeechServiceType type) async { if (selectedServiceType.value == type) return; - + try { isProcessing.value = true; - + // 停止当前的所有服务状态 await stopAll(); - + // 切换服务类型 _speechFactory.setServiceType(type); selectedServiceType.value = type; - + // 重新初始化服务 await _initServices(); } catch (e) { @@ -202,40 +203,40 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止所有活动 Future stopAll() async { // 停止ASR if (isListening.value) { await stopContinuousRecognition(); } - + // 停止TTS if (isSpeaking.value) { await stopSpeaking(); } - + // 取消所有订阅 await _recognitionSubscription?.cancel(); _recognitionSubscription = null; } - + /// 单次播放文本 Future speakOnce() async { if (!isInitialized.value || isProcessing.value) return; - + final text = textController.text.trim(); if (text.isEmpty) { ttsStatus.value = '请输入要播放的文本'; return; } - + try { isProcessing.value = true; ttsStatus.value = '准备播放...'; - + final result = await _ttsService.speakOnce(text); - + if (!result) { ttsStatus.value = '播放失败'; } @@ -246,15 +247,15 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 连续播放测试 Future speakContinuously() async { if (!isInitialized.value || isProcessing.value) return; - + try { isProcessing.value = true; ttsStatus.value = '准备连续播放...'; - + // 为避免阻塞,使用一个句子接一个句子的方式添加到队列 for (final sentence in testSentences) { if (!await _ttsService.speakStream(sentence)) { @@ -264,10 +265,9 @@ class SpeechTestController extends GetxController { // 给前一个句子一点处理时间 await Future.delayed(const Duration(milliseconds: 50)); } - + // 刷新队列,确保播放完成 await _ttsService.flushStream(); - } catch (e) { ttsStatus.value = '连续播放异常: $e'; Logger.error('连续播放异常: $e'); @@ -275,11 +275,11 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止播放 Future stopSpeaking() async { if (!isInitialized.value) return; - + try { await _ttsService.stop(); } catch (e) { @@ -287,39 +287,39 @@ class SpeechTestController extends GetxController { Logger.error('停止播放异常: $e'); } } - + /// 一次性识别 Future recognizeOnce() async { if (!isInitialized.value || isProcessing.value || isListening.value) return; - + try { isProcessing.value = true; recognitionStatus.value = '开始识别...'; - + // 清空之前的结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; - + // 显示正在进行的状态 recognitionStatus.value = '正在聆听...'; - + // 执行一次性识别 - 这是同步等待结果的操作 final result = await _asrService.recognizeOnce(); - + if (result.isError) { recognitionStatus.value = '识别错误: ${result.error}'; Logger.error('一次性识别错误: ${result.error}'); } else { // 更新最终结果 finalResult.value = result.text; - + // 更新语言信息 if (result.detectedLanguage.isNotEmpty) { detectedLanguage.value = result.detectedLanguage; Logger.debug('检测到语言: ${result.detectedLanguage}'); } - + recognitionStatus.value = '识别完成'; } } catch (e) { @@ -329,26 +329,26 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 开始连续识别 Future startContinuousRecognition() async { if (!isInitialized.value || isProcessing.value || isListening.value) return; - + try { isProcessing.value = true; - + // 清空之前的结果 intermediateResult.value = ''; finalResult.value = ''; detectedLanguage.value = ''; recognitionStatus.value = '准备连续识别...'; - + // 获取识别事件流 - 这启动了异步识别过程 - final stream = await _asrService.startContinuousRecognition(); - + final stream = + await _asrService.startContinuousRecognition(_audioSourceType); + // 设置事件监听 - 异步处理识别结果 _setupAsrEventListener(stream); - } catch (e) { recognitionStatus.value = '启动连续识别异常: $e'; Logger.error('启动连续识别异常: $e'); @@ -359,19 +359,19 @@ class SpeechTestController extends GetxController { isProcessing.value = false; } } - + /// 停止连续识别 Future stopContinuousRecognition() async { if (!isInitialized.value || !isListening.value) return; - + try { // 首先更新状态,防止重复调用 isListening.value = false; recognitionStatus.value = '正在停止识别...'; - + // 停止识别过程 await _asrService.stopContinuousRecognition(); - + // 更新UI状态 recognitionStatus.value = '连续识别已停止'; intermediateResult.value = ''; @@ -380,16 +380,16 @@ class SpeechTestController extends GetxController { Logger.error('停止连续识别异常: $e'); } } - + // 获取识别结果(用于UI显示) String getDisplayResult() { // 如果有最终结果,优先显示 String displayText = ''; - + if (finalResult.value.isNotEmpty) { displayText = finalResult.value; } - + // 如果有中间结果,添加到最终结果后(可在UI中使用不同样式显示) if (intermediateResult.value.isNotEmpty) { if (displayText.isNotEmpty) { @@ -398,19 +398,19 @@ class SpeechTestController extends GetxController { displayText = '正在识别: ${intermediateResult.value}'; } } - + return displayText; } - + @override void onClose() { // 取消所有订阅 _recognitionSubscription?.cancel(); _ttsEventSubscription?.cancel(); - + // 释放控制器 textController.dispose(); - + super.onClose(); } -} \ No newline at end of file +} diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart index fc97056c5..7962d629f 100644 --- a/lib/modules/translation/controllers/translation_controller.dart +++ b/lib/modules/translation/controllers/translation_controller.dart @@ -9,28 +9,9 @@ import '../../../data/services/language_manager.dart'; import '../../../core/utils/logger.dart'; import '../../../data/models/events/voice_interaction_event.dart'; import '../../../data/services/asr_service.dart'; -import 'package:jieli/jieli.dart'; +import '../../../data/services/ble_manager.dart'; class TranslationController extends GetxController { - late Jieli jieli; - // 事件订阅 - StreamSubscription? _scanSubscription; - StreamSubscription? _connectionSubscription; - StreamSubscription? _wakeupSubscription; - StreamSubscription? _audioSubscription; - StreamSubscription? _commandResultSubscription; - // 状态变量 - var isScanning = false.obs; - var deviceInfo = '未获取'.obs; - var connectStatus = '未连接'.obs; - var currentDevice = Rx(null); - var scannedDevices = RxList([]); - var wakeupReceived = false.obs; - var audioDataSize = 0.obs; - var receivedFrames = 0.obs; - var audioDataReceiving = false.obs; - var volumeCommandResult = ''.obs; - // 服务 final AsrService _asrService = Get.find(); final VolcanoTranslationService _translationService = @@ -38,7 +19,10 @@ class TranslationController extends GetxController { final TtsService _ttsService = Get.find(); final LanguageManager _languageManager = Get.find(); final GetStorage _storage = GetStorage(); - + // 蓝牙服务 + final _bleManager = Get.find(); + // 音频输入源 + bool _audioSourceType = false; // 存储相关 static const String _historyKey = 'translation_history'; static const String _sourceLanguageKey = 'translation_source_language'; @@ -104,7 +88,7 @@ class TranslationController extends GetxController { @override void onInit() { super.onInit(); - jieli = Jieli(); + //_setupEventListeners(); // 加载上次保存的语言设置 _loadSavedLanguages(); @@ -240,19 +224,23 @@ class TranslationController extends GetxController { // // 添加到历史记录 // translationHistory.add(newItem); + // 开始连续语音识别 if (currentMode.value == "audioVideo") { + _bleManager.openDecoder(); // 发送ble音乐或者通话远端声音 - + _audioSourceType = true; Logger.info('发送ble音乐或者通话远端声音'); } else if (currentMode.value == "call") { // 发送ble系统mic和dac(音乐或者通话远端)声音 - + _bleManager.openA2DPDecoder(); + _audioSourceType = true; Logger.info('发送ble系统mic和dac(音乐或者通话远端)声音'); + } else { + // 开始连续语音识别 + _audioSourceType = false; } - - // 开始连续语音识别 - final recognitionStream = await _asrService.startContinuousRecognition(); - + final recognitionStream = + await _asrService.startContinuousRecognition(_audioSourceType); isRecognizing.value = true; // 监听识别结果 diff --git a/lib/routes/app_pages.dart b/lib/routes/app_pages.dart index 9848b2921..8f36c4510 100644 --- a/lib/routes/app_pages.dart +++ b/lib/routes/app_pages.dart @@ -41,6 +41,8 @@ import '../modules/speech_test/views/speech_test_view.dart'; import '../modules/speech_test/bindings/speech_test_binding.dart'; import '../modules/agent/views/agent_view.dart'; import '../modules/agent/bindings/agent_binding.dart'; +import '../modules/ota_upgrade/views/ota_upgrade_view.dart'; +import '../modules/ota_upgrade/bindings/ota_upgrade_binding.dart'; abstract class AppPages { static final pages = [ @@ -91,6 +93,16 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), + GetPage( + name: Routes.recording, + page: () => const ChatView(), + binding: ChatBinding(), + ), + GetPage( + name: Routes.OTA_UPGRADE, + page: () => const OtaUpgradeView(), + binding: OtaUpgradeBinding(), + ), GetPage( name: Routes.profile, page: () => const ProfileView(), @@ -182,7 +194,6 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), - GetPage( name: Routes.speechTest, page: () => const SpeechTestView(), @@ -190,7 +201,6 @@ abstract class AppPages { transition: Transition.rightToLeft, transitionDuration: Duration(milliseconds: 250), ), - GetPage( name: Routes.agent, page: () => const AgentView(), diff --git a/lib/routes/app_routes.dart b/lib/routes/app_routes.dart index 41a7997c1..d44a5616c 100644 --- a/lib/routes/app_routes.dart +++ b/lib/routes/app_routes.dart @@ -6,6 +6,8 @@ abstract class Routes { static const home = '/home'; static const explore = '/explore'; static const chat = '/chat'; + static const OTA_UPGRADE = '/ota_upgrade'; + static const recording = '/recording'; static const profile = '/profile'; static const meeting = '/meeting'; static const meetingRecord = '/meetingRecord'; diff --git a/local_plugins/agent_service/android/build.gradle.kts b/local_plugins/agent_service/android/build.gradle.kts index b6c1b4d21..5a0a26504 100644 --- a/local_plugins/agent_service/android/build.gradle.kts +++ b/local_plugins/agent_service/android/build.gradle.kts @@ -48,5 +48,4 @@ dependencies { add("compileOnly", project(":deepvoice_utils")) add("compileOnly", project(":chat_storage")) add("compileOnly", project(":ble_service")) - add("compileOnly", project(":music_service")) } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/AndroidManifest.xml b/local_plugins/agent_service/android/src/main/AndroidManifest.xml index ced0df1dc..ac04b7dca 100644 --- a/local_plugins/agent_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/agent_service/android/src/main/AndroidManifest.xml @@ -14,29 +14,29 @@ android:exported="true" android:enabled="true" android:foregroundServiceType="mediaPlayback"> - + - - + --> - - + --> \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 92a329613..3bea170d1 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.* import java.util.Collections import kotlin.coroutines.CoroutineContext import com.yunqiinnovation.ble_service.BleService +import android.media.MediaPlayer +import com.yunqiinnovation.agent_service.R /** * 代理服务事件监听接口 */ @@ -69,6 +71,9 @@ object AgentService : CoroutineScope { // 系统提示词 private var systemPrompt = "" + // 音频播放器 + private var audioPlayer: AudioPlayer? = null // 初始化音频播放器 + // 状态 private var isInitialized = false var isRecognitionActive = false @@ -121,6 +126,8 @@ object AgentService : CoroutineScope { this.context = appContext.applicationContext this.openAIService = OpenAIService(context) this.chatStorageHelper = ChatStorageHelper(context) + this.audioPlayer = AudioPlayer(context) + // 从配置中获取参数 azureSpeechKey = config["azureSpeechKey"] as? String ?: "" @@ -132,6 +139,7 @@ object AgentService : CoroutineScope { // 自定义系统提示词 val customSystemPrompt = config["systemPrompt"] as? String + FileLogger.i(TAG, "初始化 系统提示词:$config") if (!customSystemPrompt.isNullOrBlank()) { systemPrompt = customSystemPrompt } else { @@ -289,11 +297,11 @@ object AgentService : CoroutineScope { // 检查录音权限 - // if (!isExternalActive||!checkRecordAudioPermission()) { - // FileLogger.e(TAG, "需要录音权限,请在设置中授予权限") - // sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) - // return false - // } + if (!isExternalActive&&!checkRecordAudioPermission()) { + FileLogger.e(TAG, "需要录音权限,请在设置中授予权限") + sendEvent("error", mapOf("code" to "PERMISSION_DENIED", "message" to "需要录音权限")) + return false + } isRecognitionActive = true hasSpeechDetected = false @@ -354,12 +362,14 @@ object AgentService : CoroutineScope { sendEvent("recognition_started", mapOf("status" to "started")) // 启动空闲检测 startIdleCheck() + audioPlayer?.playAudio(R.raw.start) } override fun onSessionStopped() { isRecognitionActive = false stopIdleCheck() sendEvent("recognition_stopped", mapOf("status" to "stopped")) + audioPlayer?.playAudio(R.raw.stop) } override fun onCanceled(reason: String, errorDetails: String) { @@ -439,24 +449,25 @@ object AgentService : CoroutineScope { */ private fun stopAiStream() { if (isAiStreaming) { - // 取消当前AI生成任务 - currentAiJob?.cancel() - currentAiJob = null - - // 通知OpenAI服务终止当前流式请求 - openAIService.cancelCurrentStream() - - // 更新状态 - isAiStreaming = false - - // 记录日志 - FileLogger.d(TAG, "AI流输出已停止") - - // // 向客户端发送取消事件,表明AI回复已被终止 - // sendEvent("assistant_stream_canceled", mapOf( - // "status" to "canceled", - // "timestamp" to System.currentTimeMillis() - // )) + try { + // 先更新状态,避免回调时的状态不一致 + isAiStreaming = false + + // 取消当前AI生成任务 + currentAiJob?.cancel() + currentAiJob = null + + // 通知OpenAI服务终止当前流式请求 + openAIService.cancelCurrentStream() + + // 记录日志 + FileLogger.d(TAG, "AI流输出已停止") + } catch (e: Exception) { + FileLogger.e(TAG, "停止AI流输出异常", e) + // 确保状态被重置,即使发生异常 + isAiStreaming = false + currentAiJob = null + } } } @@ -559,6 +570,7 @@ object AgentService : CoroutineScope { // 使用历史记录作为上下文发送到OpenAI val responseBuilder = StringBuilder() + var metadata:String = "" // 添加系统提示词到历史消息的副本中 val messagesWithSystemPrompt = JSONArray() @@ -637,7 +649,7 @@ object AgentService : CoroutineScope { addToHistoryMessages(openAIService.createAssistantMessage(response)) // 保存聊天记录 - saveChatMessage(displayText, response) + saveChatMessage(displayText, response,metadata) } // 标记AI流式输出已完成 @@ -658,12 +670,17 @@ object AgentService : CoroutineScope { } override fun onFunctionCall(call: JSONObject) { - sendEvent("function_call", mapOf( - "name" to call.getString("name"), - "arguments" to call.toString() - )) - if(call.getString("name") == "exit_interaction") { - stopRecognition() + try { + val name = call.getString("name") + sendEvent("function_call", mapOf( + "name" to name, + "arguments" to call.toString() + )) + if(name == "exit_interaction") { + stopRecognition() + } + } catch (e: Exception) { + FileLogger.e(TAG, "处理函数调用事件失败", e) } } @@ -673,19 +690,6 @@ object AgentService : CoroutineScope { "result" to functionCallResult.toString(), )) FileLogger.d(TAG, "mcp调用结果: $functionCallResult") - val metaStr = functionCallResult.optString("meta") - if (metaStr.isNotEmpty()) { - val meta = JSONObject(metaStr) - FileLogger.d(TAG, "mcp调用结果: $metaStr") - if (meta.has("card_music")) { - val cardMusic = meta.getJSONObject("card_music") - FileLogger.d(TAG, "检查到音乐卡片: $cardMusic") - val musicUrl = cardMusic.optString("url", "") - val name = cardMusic.optString("name", "") - val sgener = cardMusic.optString("sgener", "") - processMusicPlay(musicUrl, name, sgener) - } - } } } ) @@ -784,7 +788,7 @@ object AgentService : CoroutineScope { /** * 保存聊天消息 */ - private fun saveChatMessage(userMessage: String, assistantMessage: String) { + private fun saveChatMessage(userMessage: String, assistantMessage: String,metadata : String) { launch(Dispatchers.IO) { try { // 保存用户消息 @@ -799,7 +803,8 @@ object AgentService : CoroutineScope { val assistantMessageId = chatStorageHelper.saveMessage( sessionId = sessionId, message = assistantMessage, - sender = "assistant" + sender = "assistant", + metadata = metadata ) // if (assistantMessageId != -1L) { @@ -864,25 +869,43 @@ object AgentService : CoroutineScope { } /** - * 释放资源 + * 释放所有资源 */ fun dispose() { - stopRecognition() - stopTts() - stopAiStream() - stopIdleCheck() - - azureAsrHelper?.dispose() - azureTtsHelper?.dispose() - job.cancel() - - // 清除所有监听器 - clearListeners() - - // 重置单例状态,以便下次使用时可以重新初始化 - isInitialized = false - - FileLogger.d(TAG, "代理服务资源已释放") + try { + // 停止语音识别 + stopRecognition() + + // 停止TTS播放 + stopTts() + + // 停止AI流输出 + stopAiStream() + + // 停止空闲检测 + stopIdleCheck() + + // 释放音频播放器资源 + audioPlayer?.release() + audioPlayer = null + + // 释放Azure服务 + azureAsrHelper?.dispose() + azureTtsHelper?.dispose() + + // 取消所有协程 + job.cancel() + + // 清除监听器 + clearListeners() + + // 标记为未初始化 + isInitialized = false + + FileLogger.d(TAG, "代理服务资源已释放") + } catch (e: Exception) { + FileLogger.e(TAG, "释放资源异常", e) + } } /** @@ -964,17 +987,57 @@ object AgentService : CoroutineScope { } /** - * 处理音乐播放 - * 将ai返回的音乐连接 在后台启动播放 - * - * @param musicUrl 图片文件路径 - * @param name 歌曲名称,默认为空 - * @param singer 歌手,默认为false - * @return 是否成功开始处理 + * 简单音频播放器,用于播放提示音等 */ - fun processMusicPlay(musicUrl:String,name: String = "",singer:String = ""){ - // 在其他 Service、BroadcastReceiver 或 Application 中调用 - FileLogger.i(TAG, "启动音乐服务 播放音乐 $musicUrl") - MusicServiceStarter.startServiceWithCommand(context, "play", url = musicUrl) + class AudioPlayer(private val context: Context) { + private var mediaPlayer: MediaPlayer? = null + + /** + * 播放音频资源 + * @param resId 资源ID + */ + fun playAudio(resId: Int) { + try { + // 释放之前的资源 + release() + + // 创建播放器并设置资源 + mediaPlayer = MediaPlayer().apply { + // 设置资源 + context.resources.openRawResourceFd(resId)?.use { fd -> + setDataSource(fd.fileDescriptor, fd.startOffset, fd.length) + } + + // 播放完成后自动释放资源 + setOnCompletionListener { + release() + } + + // 准备并播放 + prepare() + start() + } + + FileLogger.d(TAG, "开始播放音频资源") + } catch (e: Exception) { + FileLogger.e(TAG, "播放音频资源异常: ${e.message}", e) + release() + } + } + + /** + * 释放资源 + */ + fun release() { + try { + mediaPlayer?.apply { + if (isPlaying) stop() + release() + } + mediaPlayer = null + } catch (e: Exception) { + // 忽略异常 + } + } } } \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt index 588e66ac5..1fc35fafa 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentServicePlugin.kt @@ -57,6 +57,7 @@ class AgentServicePlugin : FlutterPlugin, MethodCallHandler, EventChannel.Stream config["openaiApiKey"]?.toString()?.let { intent.putExtra("openaiApiKey", it) } config["openaiBaseUrl"]?.toString()?.let { intent.putExtra("openaiBaseUrl", it) } config["openaiModel"]?.toString()?.let { intent.putExtra("openaiModel", it) } + config["systemPrompt"]?.toString()?.let { intent.putExtra("systemPrompt", it) } config["mcpServer"]?.toString()?.let { intent.putExtra("mcpServer", it) } // 启动前台服务 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt index f362b478d..23d39b554 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleAgent.kt @@ -142,7 +142,7 @@ object BleAgent : BleService.Callback, AgentServiceListener { // 停止当前TTS,避免冲突 AgentService.interruptCurrentResponse() - AgentService.speakText("我在!") + // AgentService.speakText("我在!") // 启动语音识别 if (AgentService.isRecognitionActive) { // 如果已经在识别,则重新启动 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt index 21c2cf5c4..83c46a4db 100644 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt +++ b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleMediaService.kt @@ -20,7 +20,7 @@ import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat import androidx.core.app.NotificationCompat import androidx.media.session.MediaButtonReceiver -import com.yunqiinnovation.ble_service.R +import com.yunqiinnovation.agent_service.R import com.yunqiinnovation.deepvoice_utils.FileLogger import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob @@ -54,6 +54,7 @@ class BleMediaService : Service(), CoroutineScope { private const val KEY_OPENAI_API_KEY = "openaiApiKey" private const val KEY_OPENAI_BASE_URL = "openaiBaseUrl" private const val KEY_OPENAI_MODEL = "openaiModel" + private const val KEY_SYSTEM_PROMPT = "systemPrompt" private const val KEY_MCP_SERVER = "mcpServer" // 用于跟踪服务是否正在运行 @@ -70,7 +71,6 @@ class BleMediaService : Service(), CoroutineScope { private lateinit var mediaSession: MediaSessionCompat private lateinit var audioManager: AudioManager - private var isPlaying = false // 媒体操作防抖 @@ -94,7 +94,7 @@ class BleMediaService : Service(), CoroutineScope { // 设置服务状态 isRunning.set(true) - + startForeground(NOTIFICATION_ID, createNotification()) // 初始化音频管理器 audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -102,23 +102,20 @@ class BleMediaService : Service(), CoroutineScope { setupMediaSession() // 注册媒体按钮接收器 - registerMediaButtonReceiver() - // 注册后台扫描 - registerBackgroundScan() + // registerMediaButtonReceiver() + // // 注册后台扫描 + // registerBackgroundScan() // 创建音频播放器,并播放静音文件 audioPlayer = MiniMediaPlayer(this) audioPlayer?.play(R.raw.silence, 0f) setPlaybackState(PlaybackStateCompat.STATE_PAUSED) - // 确保尽快启动前台服务,防止系统杀死服务 - startForeground(NOTIFICATION_ID, createNotification()) - FileLogger.i(TAG, "媒体服务已启动") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") + // FileLogger.i(TAG, "BleMediaService.onStartCommand: ${intent?.action}") // 保存当前Intent,用于后续初始化 currentIntent = intent @@ -134,18 +131,12 @@ class BleMediaService : Service(), CoroutineScope { // 处理蓝牙扫描结果事件 if (intent?.action == BleConst.ACTION_SCAN_RESULT) { - val mac = intent.getStringExtra("mac") - if (mac != null) { val currentTime = System.currentTimeMillis() if (currentTime - lastScanTime >= ACTION_DEBOUNCE_MS) { lastScanTime = currentTime - checkBleConnection(mac) - } else { - // FileLogger.i(TAG, "扫描操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复MAC请求") - } - } else { - FileLogger.w(TAG, "收到扫描结果但MAC地址为空") - } + checkBleConnection() + } + return START_STICKY } @@ -213,6 +204,7 @@ class BleMediaService : Service(), CoroutineScope { it.getStringExtra(KEY_OPENAI_API_KEY)?.let { value -> config[KEY_OPENAI_API_KEY] = value } it.getStringExtra(KEY_OPENAI_BASE_URL)?.let { value -> config[KEY_OPENAI_BASE_URL] = value } it.getStringExtra(KEY_OPENAI_MODEL)?.let { value -> config[KEY_OPENAI_MODEL] = value } + it.getStringExtra(KEY_SYSTEM_PROMPT)?.let { value -> config[KEY_SYSTEM_PROMPT] = value } it.getStringExtra(KEY_MCP_SERVER)?.let { value -> config[KEY_MCP_SERVER] = value } } FileLogger.i(TAG, "从Intent中提取到配置参数: $config") @@ -243,6 +235,7 @@ class BleMediaService : Service(), CoroutineScope { KEY_OPENAI_API_KEY to getConfigString(prefs, KEY_OPENAI_API_KEY, ""), KEY_OPENAI_BASE_URL to getConfigString(prefs, KEY_OPENAI_BASE_URL, "https://api.openai.com/v1"), KEY_OPENAI_MODEL to getConfigString(prefs, KEY_OPENAI_MODEL, "gpt-3.5-turbo"), + KEY_SYSTEM_PROMPT to getConfigString(prefs, KEY_SYSTEM_PROMPT, "gpt-3.5-turbo"), KEY_MCP_SERVER to getConfigString(prefs, KEY_MCP_SERVER, "") ) } @@ -297,24 +290,7 @@ class BleMediaService : Service(), CoroutineScope { setCallback(object : MediaSessionCompat.Callback() { override fun onMediaButtonEvent(mediaButtonIntent: Intent): Boolean { - FileLogger.i(TAG, "onMediaButtonEvent: ${mediaButtonIntent.action}") - // 这里可以添加切换播放/暂停状态的逻辑 - isPlaying = !isPlaying - - // 更新播放状态 - val newState = if (isPlaying) { - PlaybackStateCompat.STATE_PLAYING - } else { - PlaybackStateCompat.STATE_PAUSED - } - - // 设置新的播放状态 - setPlaybackState(newState) - - // 更新通知 - updateNotification() - handleMediaAction() // 使用系统默认的媒体按钮处理 return super.onMediaButtonEvent(mediaButtonIntent) } @@ -348,98 +324,24 @@ class BleMediaService : Service(), CoroutineScope { FileLogger.e(TAG, "媒体按钮接收器注册失败: ${e.message}", e) } } - /** - * 处理媒体操作 - * 包含防抖逻辑,避免短时间内重复触发 - */ - private fun handleMediaAction() { - // FileLogger.i(TAG, "handleMediaAction") - // // 防抖处理逻辑 - // val currentTime = System.currentTimeMillis() - // if (currentTime - lastActionTime >= ACTION_DEBOUNCE_MS) { - // lastActionTime = currentTime - // BleAgent.onWakeupSignalReceived() - // } else { - // FileLogger.i(TAG, "媒体操作防抖: 忽略${ACTION_DEBOUNCE_MS}ms内的重复请求") - // } - - } - private fun checkBleConnection(mac: String) { - FileLogger.i(TAG, "handleMediaAction收到连接请求: ${mac}") + private fun checkBleConnection() { + + FileLogger.i(TAG, "hcheckBleConnection收到连接请求") try { // 示例:使用协程异步处理连接 launch { if(!BleService.isConnected()) { - BleService.connect(mac) + BleService.connect() } } } catch (e: Exception) { FileLogger.e(TAG, "连接设备失败: ${e.message}", e) } } - // ====================================================================================================== - // 后台扫描与唤醒 - // ====================================================================================================== - /** - * 注册后台持久扫描,APP被杀死后也能唤醒 - */ - fun registerBackgroundScan() { - val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return - try { - val filter = ScanFilter.Builder() - .setManufacturerData(0xEFAB, byteArrayOf(0xA0.toByte(), 0x01)) - .build() - - val settings = ScanSettings.Builder() - .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) - .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) - .build() - - // 创建Intent并设置前台接收器标志 - val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(this, BleWakeReceiver::class.java) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - - val pi = PendingIntent.getBroadcast( - this, 0, scanIntent, - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE - } else { - PendingIntent.FLAG_UPDATE_CURRENT - } - ) - scanner.startScan(listOf(filter), settings, pi) - FileLogger.i(TAG, "后台持久扫描已注册") - } catch (e: Exception) { - FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) - } - } - - /** - * 注销后台持久扫描 - */ - fun unregisterBackgroundScan() { - // 获取一个局部引用,避免并发访问问题 - val scanner = (getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.bluetoothLeScanner ?: return - try { - // 创建Intent并设置前台接收器标志 - val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) - .setClass(this, BleWakeReceiver::class.java) - .addFlags(Intent.FLAG_RECEIVER_FOREGROUND) - - val pi = PendingIntent.getBroadcast( - this, 0, scanIntent, - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE - ) - scanner.stopScan(pi) - FileLogger.i(TAG, "后台持久扫描已注销") - } catch (e: Exception) { - FileLogger.e(TAG, "注销后台扫描失败: ${e.message}", e) - } - } + /** * 更新通知内容 */ @@ -512,7 +414,7 @@ class BleMediaService : Service(), CoroutineScope { // 构建通知 val builder = NotificationCompat.Builder(this, CHANNEL_ID) .setContentTitle("DeepSound") - .setContentText(if (isPlaying) "正在播放" else "已暂停") + // .setContentText(if (isPlaying) "正在播放" else "已暂停") .setSmallIcon(android.R.drawable.ic_media_play) // 设置点击意图 diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt deleted file mode 100644 index 2c8479e29..000000000 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt +++ /dev/null @@ -1,85 +0,0 @@ -package com.yunqiinnovation.agent_service - -import android.content.* -import android.bluetooth.le.* -import android.os.Build -import android.view.KeyEvent -import android.media.AudioManager -import com.yunqiinnovation.deepvoice_utils.FileLogger -import com.yunqiinnovation.ble_service.BleService -import com.yunqiinnovation.ble_service.BleConst -/** - * BLE唤醒接收器 - * 当耳机发送含特定UUID广播时,系统会触发此接收器 - */ -class BleWakeReceiver : BroadcastReceiver() { - - companion object { - private const val TAG = "BleWakeReceiver" - } - - override fun onReceive(ctx: Context, intent: Intent) { - FileLogger.d(TAG, "onReceive, intent: $intent, ${BleConst.ACTION_SCAN_RESULT}") - FileLogger.init(ctx) - try { - - if (intent.action != BleConst.ACTION_SCAN_RESULT) { - return - } - val list = intent.getParcelableArrayListExtra( - BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT - ) - // FileLogger.d(TAG, "list: $list") - - if (list?.isEmpty() == true || list == null) { - FileLogger.d(TAG, "扫描结果为空") - return - } - - val mac = list.firstOrNull()?.device?.address - if (mac == null) { - FileLogger.d(TAG, "没有获取到设备MAC地址") - return - } - - FileLogger.d(TAG, "被系统唤醒,发现设备 $mac") - - if(!BleMediaService.isServiceRunning()) { - val am = ctx.getSystemService(Context.AUDIO_SERVICE) as AudioManager - - val keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK) - am.dispatchMediaKeyEvent(keyEvent) - // val mediaIntent = Intent(Intent.ACTION_MEDIA_BUTTON) - // mediaIntent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent) - // mediaIntent.putExtra("mac", mac) // 额外传递MAC地址 - - // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // // 通过AudioManager发送媒体按键 - // am.dispatchMediaKeyEvent(keyEvent) - // // 发送UP事件 - // // am.dispatchMediaKeyEvent(KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)) - // } else { - // // 通过广播发送媒体按钮 - // mediaIntent.component = ComponentName(ctx.packageName, "androidx.media.session.MediaButtonReceiver") - // ctx.sendBroadcast(mediaIntent) - // } - } - if (!BleService.isConnected()) { - val serviceIntent = Intent(ctx, BleMediaService::class.java).apply { - action = BleConst.ACTION_SCAN_RESULT - putExtra("mac", mac) - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - ctx.startForegroundService(serviceIntent) - } else { - ctx.startService(serviceIntent) - } - return - } - } - catch (e: Exception) { - FileLogger.e(TAG, "接收器异常: ${e}") - } - } -} \ No newline at end of file diff --git a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt b/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt deleted file mode 100644 index 4f490ae88..000000000 --- a/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.yunqiinnovation.agent_service - -import android.content.* -import android.os.Build - -/** - * 系统启动接收器 - * 当系统重启完成后,自动启动BLE前台服务 - * 注意:需要用户在设置中授予应用自启动权限才能生效 - */ -class BootReceiver : BroadcastReceiver() { - override fun onReceive(ctx: Context, i: Intent) { - if (i.action == Intent.ACTION_BOOT_COMPLETED) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - ctx.startForegroundService(Intent(ctx, BleMediaService::class.java)) - else ctx.startService(Intent(ctx, BleMediaService::class.java)) - } - } -} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/res/raw/silence.mp3 b/local_plugins/agent_service/android/src/main/res/raw/silence.mp3 similarity index 100% rename from local_plugins/ble_service/android/src/main/res/raw/silence.mp3 rename to local_plugins/agent_service/android/src/main/res/raw/silence.mp3 diff --git a/local_plugins/agent_service/android/src/main/res/raw/start.mp3 b/local_plugins/agent_service/android/src/main/res/raw/start.mp3 new file mode 100644 index 000000000..b90292446 Binary files /dev/null and b/local_plugins/agent_service/android/src/main/res/raw/start.mp3 differ diff --git a/local_plugins/agent_service/android/src/main/res/raw/stop.mp3 b/local_plugins/agent_service/android/src/main/res/raw/stop.mp3 new file mode 100644 index 000000000..4ab6124b4 Binary files /dev/null and b/local_plugins/agent_service/android/src/main/res/raw/stop.mp3 differ diff --git a/local_plugins/agent_service/android/src/main/res/raw/stop2.mp3 b/local_plugins/agent_service/android/src/main/res/raw/stop2.mp3 new file mode 100644 index 000000000..92e13209c Binary files /dev/null and b/local_plugins/agent_service/android/src/main/res/raw/stop2.mp3 differ 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 9a97dce58..f149fa2be 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 @@ -172,7 +172,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } } "startContinuousRecognition" -> { - val useExternalAudio = call.argument("useExternalAudio") ?: false + val useExternalAudio = call.argument("audioSourceType") ?: false // 确保事件通道已准备好 if (asrEventSink == null) { result.error("EVENT_CHANNEL_NOT_READY", "事件通道未准备好,无法开始连续识别", null) @@ -187,6 +187,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { } else { AzureAsrHelper.AudioSourceType.MICROPHONE } + val success = azureAsrHelper.startContinuousRecognition(object : AzureAsrHelper.ContinuousRecognizeCallback { override fun onResult(text: String, detectedLanguage: String) { sendAsrEvent(mapOf( @@ -261,7 +262,7 @@ class AzureSpeechPlugin: FlutterPlugin, CoroutineScope { "initialize" -> { val subscriptionKey = call.argument("subscriptionKey") ?: "" val region = call.argument("region") ?: "" - val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: true // 默认使用自定义播放器 + val useCustomAudioOutput = call.argument("useCustomAudioOutput") ?: false // 默认使用自定义播放器 // 先释放现有资源 azureTtsHelper.dispose() diff --git a/local_plugins/ble_service/android/src/main/AndroidManifest.xml b/local_plugins/ble_service/android/src/main/AndroidManifest.xml index 6dba89de3..f5763d6e5 100644 --- a/local_plugins/ble_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/ble_service/android/src/main/AndroidManifest.xml @@ -3,20 +3,50 @@ xmlns:tools="http://schemas.android.com/tools" package="com.yunqiinnovation.ble_service"> + + + + + + + + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt new file mode 100644 index 000000000..60a555498 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt @@ -0,0 +1,481 @@ +package com.yunqiinnovation.ble_service + +import android.app.Activity +import android.app.PendingIntent +import android.bluetooth.BluetoothDevice +import android.bluetooth.BluetoothManager +import android.bluetooth.le.ScanFilter +import android.bluetooth.le.ScanSettings +import android.companion.AssociationInfo +import android.companion.AssociationRequest +import android.companion.BluetoothDeviceFilter +import android.companion.CompanionDeviceManager +import android.content.Context +import android.content.Intent +import android.content.IntentSender +import android.content.SharedPreferences +import android.os.Build +import android.os.ParcelUuid +import androidx.annotation.RequiresApi +import com.yunqiinnovation.deepvoice_utils.FileLogger +import java.util.regex.Pattern + +/** + * BleCompanionManager + * 用于管理BLE设备的配对和关联 + */ +class BleCompanionManager(private val context: Context) { + private val TAG = "BleCompanionManager" + + // 常量定义 + companion object { + const val REQUEST_CODE_ASSOCIATE = 42 + private const val PREF_NAME = "ble_companion_prefs" + private const val KEY_ASSOCIATION_IDS = "association_ids" + private const val KEY_PAIRED_MAC = "paired_mac" + } + + // 会话保存 + private val sharedPreferences: SharedPreferences by lazy { + context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + } + + // 获取CompanionDeviceManager服务 + private val companionDeviceManager: CompanionDeviceManager? by lazy { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.getSystemService(Context.COMPANION_DEVICE_SERVICE) as? CompanionDeviceManager + } else { + null + } + } + + /** + * 检查是否支持CompanionDeviceManager + */ + fun isCompanionDeviceManagerSupported(): Boolean { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && companionDeviceManager != null + } + + /** + * 获取已关联的设备MAC地址 + */ + fun getPairedMacAddress(): String? { + return sharedPreferences.getString(KEY_PAIRED_MAC, null) + } + + /** + * 保存已关联的设备MAC地址 + */ + fun savePairedMacAddress(macAddress: String) { + sharedPreferences.edit().putString(KEY_PAIRED_MAC, macAddress).apply() + FileLogger.i(TAG, "已保存配对设备MAC: $macAddress") + } + + /** + * 清除已关联的设备MAC地址 + */ + fun clearPairedMacAddress() { + sharedPreferences.edit().remove(KEY_PAIRED_MAC).apply() + FileLogger.i(TAG, "已清除配对设备MAC") + } + + /** + * 检查是否有已关联的设备 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun hasAssociations(): Boolean { + val associationIds = getAssociationIds() + return associationIds.isNotEmpty() + } + + /** + * 获取所有关联ID + */ + @RequiresApi(Build.VERSION_CODES.O) + fun getAssociationIds(): List { + // 注意: allAssociations 仅在API 33 (Android 13)及以上版本可用 + // 在早期版本中,使用associations (返回的是String数组) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + // 由于AssociationInfo类在API 30+引入,使用反射获取更安全 + val method = CompanionDeviceManager::class.java.getMethod("getAllAssociations") + @Suppress("UNCHECKED_CAST") + val associations = method.invoke(companionDeviceManager) as? List + associations?.map { association -> + // 尝试获取AssociationInfo.id + val idField = association.javaClass.getField("id") + idField.get(association).toString() + } ?: emptyList() + } catch (e: Exception) { + FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + emptyList() + } + } else { + // API 26-32 使用 associations 方法 + companionDeviceManager?.associations?.toList() ?: emptyList() + } + } + + /** + * 开始设备关联流程 + * + * @param activity 当前Activity,用于启动系统关联对话框 + * @param deviceNamePattern 设备名称正则表达式,例如"MyHeadset.*" + * @param serviceUuid 可选的服务UUID + * @param macAddress 可选的MAC地址 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun startAssociation( + activity: Activity, + deviceNamePattern: String? = null, + serviceUuid: String? = null, + macAddress: String? = null + ): Boolean { + if (!isCompanionDeviceManagerSupported()) { + FileLogger.e(TAG, "设备不支持CompanionDeviceManager") + return false + } + + try { + // 构建设备过滤器 + val deviceFilterBuilder = BluetoothDeviceFilter.Builder() + + // 添加设备名称模式 + deviceNamePattern?.let { + deviceFilterBuilder.setNamePattern(Pattern.compile(it)) + } + + // 添加服务UUID + serviceUuid?.let { + try { + val uuid = ParcelUuid(java.util.UUID.fromString(it)) + deviceFilterBuilder.addServiceUuid(uuid, null) + } catch (e: Exception) { + FileLogger.e(TAG, "设置服务UUID失败: ${e.message}", e) + } + } + + // 添加MAC地址 - 注意:setAddress方法在不同API版本有差异 + macAddress?.let { + try { + // 检查是否是有效的MAC地址格式 + if (!isValidMacAddress(it)) { + FileLogger.e(TAG, "无效的MAC地址格式: $it") + return@let + } + + // 保存MAC地址,用于后续验证和处理 + savePairedMacAddress(it) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + try { + // Android O(8.0)及以上版本都应该使用字符串格式的MAC地址 + deviceFilterBuilder.setAddress(it) + FileLogger.i(TAG, "成功设置MAC地址过滤: $it") + } catch (e: Exception) { + FileLogger.e(TAG, "setAddress失败: ${e.message}") + // 如果设置MAC地址失败,回退到使用设备名称过滤 + deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) + } + } else { + // Android 8.0以下版本不支持MAC地址过滤,使用设备名称过滤 + FileLogger.w(TAG, "当前Android版本不支持MAC地址过滤,将使用设备名称过滤") + deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) + } + } catch (e: Exception) { + FileLogger.e(TAG, "处理MAC地址失败: ${e.message}", e) + // 发生异常时,使用通配名称模式 + deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) + } + } ?: run { + // 如果没有提供MAC地址且没有设置名称模式,则使用通配名称 + if (deviceNamePattern == null) { + deviceFilterBuilder.setNamePattern(Pattern.compile(".*")) + FileLogger.i(TAG, "未提供MAC地址,使用通配名称模式") + } + } + + // 创建请求 + val associationRequest = AssociationRequest.Builder() + .addDeviceFilter(deviceFilterBuilder.build()) + .setSingleDevice(true) // 找到一个即停止 + .build() + + // 发起关联请求 + companionDeviceManager?.associate( + associationRequest, + object : CompanionDeviceManager.Callback() { + override fun onDeviceFound(intentSender: IntentSender) { + try { + activity.startIntentSenderForResult( + intentSender, + REQUEST_CODE_ASSOCIATE, + null, + 0, + 0, + 0 + ) + FileLogger.i(TAG, "已启动设备选择对话框") + } catch (e: Exception) { + FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + } + } + + // API 26 (Android 8) 使用 onDeviceFound + // API 28+ (Android 9+) 使用 onAssociationPending + @RequiresApi(Build.VERSION_CODES.P) + override fun onAssociationPending(intentSender: IntentSender) { + try { + activity.startIntentSenderForResult( + intentSender, + REQUEST_CODE_ASSOCIATE, + null, + 0, + 0, + 0 + ) + FileLogger.i(TAG, "已启动设备选择对话框") + } catch (e: Exception) { + FileLogger.e(TAG, "启动设备选择对话框失败: ${e.message}", e) + } + } + + // API 33+ (Android 13+) 使用 onAssociationCreated + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + override fun onAssociationCreated(associationInfo: AssociationInfo) { + FileLogger.i(TAG, "设备关联已创建: ${associationInfo.displayName}, ID: ${associationInfo.id}") + // 保存关联ID + try { + val macAddressMethod = associationInfo.javaClass.getMethod("getDeviceMacAddress") + val mac = macAddressMethod.invoke(associationInfo) as? String + mac?.let { savePairedMacAddress(it) } + } catch (e: Exception) { + FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + } + } + + override fun onFailure(error: CharSequence?) { + FileLogger.e(TAG, "设备关联失败: $error") + } + }, + null + ) + + return true + } catch (e: Exception) { + FileLogger.e(TAG, "启动设备关联失败: ${e.message}", e) + return false + } + } + + /** + * 处理关联结果 + * + * @param requestCode 请求码 + * @param resultCode 结果码 + * @param data Intent数据 + * @return 是否处理了关联结果 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun handleAssociationResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + if (requestCode != REQUEST_CODE_ASSOCIATE) { + return false + } + + FileLogger.i(TAG, "收到关联结果: resultCode=$resultCode") + + if (resultCode == Activity.RESULT_OK && data != null) { + // 获取选中的蓝牙设备 + val bluetoothDevice: BluetoothDevice? = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE, BluetoothDevice::class.java) + } else { + @Suppress("DEPRECATION") + data.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE) + } + + // 获取关联ID (Android 13+) + val associationId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + // 由于EXTRA_ASSOCIATION_ID常量仅在API 33+可用,直接使用已知的常量字符串值 + // 不再尝试使用反射获取字段 + val extraAssociationIdKey = "android.companion.extra.ASSOCIATION_ID" + data.getStringExtra(extraAssociationIdKey) + } catch (e: Exception) { + FileLogger.e(TAG, "获取关联ID失败: ${e.message}", e) + null + } + } else { + null + } + + // 获取并验证MAC地址 + if (bluetoothDevice != null) { + val deviceMac = bluetoothDevice.address + val deviceName = bluetoothDevice.name ?: "未知设备" + + // 验证MAC地址格式 + if (!isValidMacAddress(deviceMac)) { + FileLogger.e(TAG, "设备 '$deviceName' 返回的MAC地址格式无效: $deviceMac") + return false + } + + // 检查是否与预期的MAC地址匹配 + val savedMac = getPairedMacAddress() + if (!savedMac.isNullOrEmpty() && savedMac != deviceMac) { + FileLogger.w(TAG, "选中的设备MAC地址 ($deviceMac) 与预期的MAC地址 ($savedMac) 不匹配,但仍会保存") + } + + // 保存新的MAC地址 + savePairedMacAddress(deviceMac) + FileLogger.i(TAG, "设备关联成功: $deviceName ($deviceMac), 关联ID: $associationId") + + // 关联成功后,自动注册后台扫描 + registerBackgroundScan() + + return true + } else { + FileLogger.e(TAG, "无法获取关联的蓝牙设备信息") + } + } else { + FileLogger.w(TAG, "设备关联被取消或失败: resultCode=$resultCode") + } + + return false + } + + /** + * 注册后台扫描 + * + * @param macAddress 可选的MAC地址,如果不提供则使用已保存的MAC地址 + */ + fun registerBackgroundScan() { + try { + val mac = getPairedMacAddress() + if (mac.isNullOrEmpty()) { + FileLogger.e(TAG, "注册后台扫描失败: 没有配对的设备MAC地址") + return + } + + val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager + val bluetoothAdapter = bluetoothManager?.adapter + val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner + + if (bluetoothLeScanner == null) { + FileLogger.e(TAG, "注册后台扫描失败: 无法获取蓝牙扫描器") + return + } + + // 构建过滤器 + val filter = ScanFilter.Builder() + .setDeviceAddress(mac) + .build() + + // 扫描设置 + val settings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .build() + + // 创建PendingIntent + val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + .setClass(context, BleWakeReceiver::class.java) + + val pendingIntent = PendingIntent.getBroadcast( + context, + 0, + scanIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + + // 启动后台扫描 + bluetoothLeScanner.startScan(listOf(filter), settings, pendingIntent) + FileLogger.i(TAG, "已注册后台扫描,目标设备MAC: $mac") + } catch (e: Exception) { + FileLogger.e(TAG, "注册后台扫描失败: ${e.message}", e) + } + } + + /** + * 取消后台扫描 + */ + fun unregisterBackgroundScan() { + try { + val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager + val bluetoothAdapter = bluetoothManager?.adapter + val bluetoothLeScanner = bluetoothAdapter?.bluetoothLeScanner + + if (bluetoothLeScanner == null) { + FileLogger.e(TAG, "取消后台扫描失败: 无法获取蓝牙扫描器") + return + } + + // 创建PendingIntent + val scanIntent = Intent(BleConst.ACTION_SCAN_RESULT) + .setClass(context, BleWakeReceiver::class.java) + + val pendingIntent = PendingIntent.getBroadcast( + context, + 0, + scanIntent, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE + } else { + PendingIntent.FLAG_UPDATE_CURRENT + } + ) + + // 停止后台扫描 + bluetoothLeScanner.stopScan(pendingIntent) + FileLogger.i(TAG, "已取消后台扫描") + } catch (e: Exception) { + FileLogger.e(TAG, "取消后台扫描失败: ${e.message}", e) + } + } + + /** + * 清除所有关联 + */ + @RequiresApi(Build.VERSION_CODES.O) + fun clearAllAssociations() { + try { + // 清除本地保存的MAC地址 + clearPairedMacAddress() + + // 获取所有关联ID + val associationIds = getAssociationIds() + + // 遍历解除关联 + for (id in associationIds) { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + companionDeviceManager?.disassociate(id) + } else { + companionDeviceManager?.disassociate(id) + } + FileLogger.i(TAG, "已解除设备关联: $id") + } catch (e: Exception) { + FileLogger.e(TAG, "解除设备关联失败: $id, ${e.message}", e) + } + } + } catch (e: Exception) { + FileLogger.e(TAG, "清除所有关联失败: ${e.message}", e) + } + } + + /** + * 验证是否是有效的MAC地址格式 + * + * @param macAddress MAC地址字符串 + * @return 是否是有效的MAC地址 + */ + private fun isValidMacAddress(macAddress: String): Boolean { + // MAC地址格式验证:XX:XX:XX:XX:XX:XX 或 XX-XX-XX-XX-XX-XX + val macPattern = Pattern.compile( + "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$" + ) + return macPattern.matcher(macAddress).matches() + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt new file mode 100644 index 000000000..0de2320cf --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt @@ -0,0 +1,73 @@ +package com.yunqiinnovation.ble_service + +import android.companion.AssociationInfo +import android.companion.CompanionDeviceService +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.annotation.RequiresApi +import com.yunqiinnovation.deepvoice_utils.FileLogger + +/** + * BleCompanionService + * 用于监听配对设备的出现和消失 + * 注:此服务仅在Android 11 (API 30)及以上版本可用 + */ +@RequiresApi(Build.VERSION_CODES.R) +class BleCompanionService : CompanionDeviceService() { + private val TAG = "BleCompanionService" + + override fun onCreate() { + super.onCreate() + FileLogger.init(applicationContext) + FileLogger.i(TAG, "BleCompanionService已创建") + } + + override fun onDeviceAppeared(info: AssociationInfo) { + super.onDeviceAppeared(info) + FileLogger.i(TAG, "检测到配对设备出现: ${info.displayName}, ID: ${info.id}") + + // 当配对设备出现时,启动前台服务 + try { + val serviceIntent = Intent().apply { + // 尝试使用BleMediaService,如果不存在则回退到BleService + val serviceClassName = try { + Class.forName("com.yunqiinnovation.agent_service.BleMediaService").name + } catch (e: ClassNotFoundException) { + BleService::class.java.name + } + + setClassName(this@BleCompanionService, serviceClassName) + action = "android.intent.action.DEVICE_APPEARED" + putExtra("assocId", info.id) + putExtra("displayName", info.displayName) + + // 如果设备信息中有MAC地址,也可以传递 + // 使用反射获取MAC地址,因为deviceMacAddress在不同API版本可能不同或不存在 + try { + val macAddressMethod = info.javaClass.getMethod("getDeviceMacAddress") + val mac = macAddressMethod.invoke(info) as? String + mac?.let { putExtra("mac", it) } + } catch (e: Exception) { + FileLogger.e(TAG, "获取设备MAC地址失败: ${e.message}", e) + } + } + + // 启动前台服务 + startForegroundService(serviceIntent) + FileLogger.i(TAG, "已启动服务") + } catch (e: Exception) { + FileLogger.e(TAG, "启动服务失败: ${e.message}", e) + } + } + + override fun onDeviceDisappeared(info: AssociationInfo) { + super.onDeviceDisappeared(info) + FileLogger.i(TAG, "配对设备消失: ${info.displayName}, ID: ${info.id}") + + // 可以选择停止服务或执行其他清理操作 + } + + // 注意: 不需要重写 onBindCompanionDeviceService 和 onUnbindCompanionDeviceService + // 这些方法在 Android 12+ 才添加,我们可以使用父类的默认实现 +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt index 4ec732c33..ef5a6553d 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleService.kt @@ -29,6 +29,7 @@ import com.jieli.jl_audio_decode.callback.OnDecodeStreamCallback import com.jieli.jl_audio_decode.callback.OnStateCallback import com.jieli.jl_audio_decode.opus.OpusManager import com.jieli.jl_audio_decode.exceptions.OpusException +import com.yunqiinnovation.ble_service.BleCompanionManager /** * BLE服务类:提供蓝牙低功耗设备的扫描、连接和通信功能 @@ -317,6 +318,33 @@ object BleService { // ====================================================================================================== // 设备连接与通信 // ====================================================================================================== + /** + * 自动连接已配对的BLE设备 + * 如果没有找到已配对的MAC地址,将不会进行连接 + */ + fun connect() { + try { + // 创建 BleCompanionManager 实例 + val companionManager = BleCompanionManager(context) + + // 获取已配对的 MAC 地址 + val pairedMac = companionManager.getPairedMacAddress() + + if (pairedMac.isNullOrEmpty()) { + FileLogger.w(TAG, "未找到已配对的设备 MAC 地址") + return + } + + FileLogger.i(TAG, "检测到已配对设备,MAC: $pairedMac,尝试连接") + + // 调用现有的 connect 方法 + connect(pairedMac) + } catch (e: Exception) { + FileLogger.e(TAG, "自动连接已配对设备失败: ${e.message}", e) + updateConnectionState(BleConst.STATE_ERROR) + } + } + /** * 连接到指定地址的BLE设备 * @param address 设备MAC地址 @@ -999,8 +1027,45 @@ object BleService { * 控制编解码 - 打开解码 */ fun openDecoder(): Boolean { + FileLogger.i(TAG, "打开编码 (设备侧)...") + + val opusStarted = startOpusStreamDecoding( + hasHeader = false, // BLE传输的Opus数据通常无文件头 + channel = 1, // 单声道 + sampleRate = 16000,// Opus推荐16kHz或更高 + packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) + // packetSize for Opus is typically in ms, e.g. 20, 40, 60. + // The library might expect this in a different unit or derive it. + // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. + ) + + + FileLogger.i(TAG, "打开解码...") + return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte(), + + BleConst.AUDIO_CHANNEL_RIGHT.toByte())) + } + /** + * 控制编解码 - 打开解码 + */ + fun openA2DPDecoder(): Boolean { + FileLogger.i(TAG, "打开编码 (设备侧)...") + + val opusStarted = startOpusStreamDecoding( + hasHeader = false, // BLE传输的Opus数据通常无文件头 + channel = 1, // 单声道 + sampleRate = 16000,// Opus推荐16kHz或更高 + packetSize = 40 // Opus常用帧长,如20ms, 40ms, 60ms。 (16000Hz * 1channel * 2bytes/sample * 0.04s = 1280 bytes/frame if PCM) + // packetSize for Opus is typically in ms, e.g. 20, 40, 60. + // The library might expect this in a different unit or derive it. + // Given JlOpusPlugin.kt, packetSize refers to Opus encoded frame duration in ms. + ) + + FileLogger.i(TAG, "打开解码...") - return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_DECODE_ON.toByte())) + return sendCommand(BleConst.CMD_CONTROL_CODEC.toByte(), byteArrayOf(BleConst.CODEC_CONTROL_A2DP_PLAY.toByte(), + + BleConst.AUDIO_CHANNEL_RIGHT.toByte())) } /** diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt index 055d791fb..e8ba005df 100644 --- a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleServicePlugin.kt @@ -21,7 +21,8 @@ import com.yunqiinnovation.deepvoice_utils.FileLogger * BleServicePlugin * 作为Flutter与原生BLE通信的插件接口 */ -class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { +class BleServicePlugin : FlutterPlugin, MethodCallHandler, ActivityAware, + PluginRegistry.ActivityResultListener, BleService.Callback { private val TAG = "BleServicePlugin" // 添加主线程Handler @@ -40,6 +41,15 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { // 事件接收器 - 只保留数据和状态通道 private var dataEventSink: EventChannel.EventSink? = null private var statusEventSink: EventChannel.EventSink? = null + + // 添加CompanionManager + private lateinit var bleCompanionManager: BleCompanionManager + + // 保存最近一次的方法调用结果回调 + private var pendingAssociationResult: Result? = null + + // 保存当前Activity + private var activity: android.app.Activity? = null override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { FileLogger.init(flutterPluginBinding.applicationContext) @@ -55,10 +65,11 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { context = flutterPluginBinding.applicationContext // 初始化BleService - BleService.initialize(context) BleService.addCallback(this) - + + // 初始化CompanionManager + bleCompanionManager = BleCompanionManager(context) } // 设置事件通道 - 简化版 @@ -90,6 +101,42 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { } } + // 实现ActivityAware接口 + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + binding.addActivityResultListener(this) + activity = binding.activity + } + + override fun onDetachedFromActivityForConfigChanges() { + // 配置变更时不做特殊处理 + activity = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + binding.addActivityResultListener(this) + activity = binding.activity + } + + override fun onDetachedFromActivity() { + // 解绑Activity时不做特殊处理 + activity = null + } + + // 实现ActivityResultListener接口 + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { + // 处理设备关联结果 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val handled = bleCompanionManager.handleAssociationResult(requestCode, resultCode, data) + if (handled) { + // 返回设备关联结果 + pendingAssociationResult?.success(true) + pendingAssociationResult = null + return true + } + } + return false + } + // 添加sendEvent函数 private fun sendEvent(eventSink: EventChannel.EventSink?, data: T, errorPrefix: String) { if (eventSink == null) return @@ -121,7 +168,6 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { // 解除事件通道 dataEventChannel?.setStreamHandler(null) statusEventChannel?.setStreamHandler(null) - } override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { @@ -132,7 +178,7 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { val connected = BleService.isConnected() result.success(connected) } - "getBatteryInfo" -> { + "getBatteryLevel" -> { val level = BleService.getBatteryInfo() result.success(level) } @@ -151,6 +197,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { "openDecoder" -> { val success = BleService.openDecoder() result.success(success) + } + "openA2DPDecoder" -> { + val success = BleService.openA2DPDecoder() + result.success(success) } "closeCodec" -> { val success = BleService.closeCodec() @@ -160,7 +210,66 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { val success = BleService.playA2DP() result.success(success) } - + // 新增Companion Device相关方法 + "isCompanionSupported" -> { + val isSupported = bleCompanionManager.isCompanionDeviceManagerSupported() + result.success(isSupported) + } + "startDeviceAssociation" -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null) + return + } + + // 保存结果回调,用于后续关联完成后返回结果 + pendingAssociationResult = result + + // 获取参数 + val deviceNamePattern = call.argument("deviceNamePattern") + val serviceUuid = call.argument("serviceUuid") + val macAddress = call.argument("macAddress") + + val currentActivity = activity + if (currentActivity == null) { + pendingAssociationResult = null + result.error("NO_ACTIVITY", "没有可用的Activity", null) + return + } + + // 启动设备关联 + val success = bleCompanionManager.startAssociation( + currentActivity, + deviceNamePattern, + serviceUuid, + macAddress + ) + + if (!success) { + pendingAssociationResult = null + result.error("FAILED", "启动设备关联失败", null) + } + // 不要立即返回结果,等待onActivityResult回调 + } + "getPairedMacAddress" -> { + val macAddress = bleCompanionManager.getPairedMacAddress() + result.success(macAddress) + } + "registerBackgroundScan" -> { + bleCompanionManager.registerBackgroundScan() + result.success(true) + } + "unregisterBackgroundScan" -> { + bleCompanionManager.unregisterBackgroundScan() + result.success(true) + } + "clearAssociations" -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + bleCompanionManager.clearAllAssociations() + result.success(true) + } else { + result.error("UNSUPPORTED", "设备不支持Companion Device Manager", null) + } + } else -> { result.notImplemented() } @@ -170,7 +279,10 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { // BleService.Callback 接口实现 override fun onScanResult(devices: List>) { - + // 将扫描结果发送到Flutter + if (devices.isNotEmpty()) { + sendEvent(statusEventSink, mapOf("type" to "scanResult", "devices" to devices), "发送扫描结果异常") + } } override fun onConnectionStateChanged(state: Int) { @@ -212,5 +324,4 @@ class BleServicePlugin : FlutterPlugin, MethodCallHandler, BleService.Callback { // 发送设备信息事件 sendEvent(statusEventSink, resultMap, "发送设备信息异常") } - } \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt new file mode 100644 index 000000000..57fb40298 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt @@ -0,0 +1,69 @@ +package com.yunqiinnovation.ble_service + +import android.bluetooth.le.ScanResult +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import com.yunqiinnovation.deepvoice_utils.FileLogger + +/** + * BleWakeReceiver + * 用于接收后台BLE扫描结果,并启动前台服务 + */ +class BleWakeReceiver : BroadcastReceiver() { + private val TAG = "BleWakeReceiver" + + override fun onReceive(context: Context, intent: Intent) { + FileLogger.init(context) + FileLogger.i(TAG, "收到BLE扫描广播: ${intent.action}") + + if (intent.action == BleConst.ACTION_SCAN_RESULT) { + // 从扫描结果中提取MAC地址 + // val scanResults = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + // intent.getParcelableArrayListExtra( + // android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT, + // ScanResult::class.java + // ) + // } else { + // @Suppress("DEPRECATION") + // intent.getParcelableArrayListExtra( + // android.bluetooth.le.BluetoothLeScanner.EXTRA_LIST_SCAN_RESULT + // ) + // } + + + startBleService(context) + + } + } + + private fun startBleService(context: Context) { + try { + // 检查是否存在BleMediaService类,如果不存在则使用BleService + val serviceClass = try { + Class.forName("com.yunqiinnovation.agent_service.BleMediaService") + } catch (e: ClassNotFoundException) { + FileLogger.i(TAG, "BleMediaService不存在") + return + } + + // 创建服务Intent,并携带MAC地址 + val serviceIntent = Intent().apply { + setClassName(context, serviceClass.name) + action = BleConst.ACTION_SCAN_RESULT + } + + // 启动前台服务 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(serviceIntent) + FileLogger.i(TAG, "已启动前台服务: ${serviceClass.name}") + } else { + context.startService(serviceIntent) + FileLogger.i(TAG, "已启动服务: ${serviceClass.name}") + } + } catch (e: Exception) { + FileLogger.e(TAG, "启动BLE服务失败: ${e.message}", e) + } + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt new file mode 100644 index 000000000..d0a03afc4 --- /dev/null +++ b/local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt @@ -0,0 +1,26 @@ +package com.yunqiinnovation.ble_service + +import android.content.* +import android.os.Build +import android.util.Log +/** + * 系统启动接收器 + * 当系统重启完成后,自动启动BLE前台服务 + * 注意:需要用户在设置中授予应用自启动权限才能生效 + */ +class BootReceiver : BroadcastReceiver() { + private val TAG = "BootReceiver" + override fun onReceive(ctx: Context, i: Intent) { + val serviceClass = try { + Class.forName("com.yunqiinnovation.agent_service.BleMediaService") + } catch (e: ClassNotFoundException) { + Log.i(TAG, "BleMediaService不存在") + return + } + if (i.action == Intent.ACTION_BOOT_COMPLETED) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + ctx.startForegroundService(Intent(ctx, serviceClass)) + else ctx.startService(Intent(ctx, serviceClass)) + } + } +} \ No newline at end of file diff --git a/local_plugins/ble_service/lib/ble_service.dart b/local_plugins/ble_service/lib/ble_service.dart index f554a8761..3b791b437 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -15,32 +15,35 @@ class BleConnectionState { /// BLE服务插件 class BleService { static final BleService _instance = BleService._internal(); - + /// 获取单例 factory BleService() => _instance; - + // 方法通道 - final MethodChannel _methodChannel = const MethodChannel('com.yunqiinnovation.ble_service'); - + final MethodChannel _methodChannel = + const MethodChannel('com.yunqiinnovation.ble_service'); + // 事件通道 - 简化为只使用数据和状态通道 late final EventChannel _dataEventChannel; late final EventChannel _statusEventChannel; - + // 事件流 late final Stream _dataStream; late final Stream _statusStream; - + // 获取事件流 Stream get onDataReceived => _dataStream; - + // 状态流直接传递原始数据 Stream get onStatusUpdated => _statusStream; - + // 内部构造函数 BleService._internal() { - _dataEventChannel = const EventChannel('com.yunqiinnovation.ble_service/data_events'); - _statusEventChannel = const EventChannel('com.yunqiinnovation.ble_service/status_events'); - + _dataEventChannel = + const EventChannel('com.yunqiinnovation.ble_service/data_events'); + _statusEventChannel = + const EventChannel('com.yunqiinnovation.ble_service/status_events'); + // 初始化事件流 _dataStream = _dataEventChannel.receiveBroadcastStream(); _statusStream = _statusEventChannel.receiveBroadcastStream(); @@ -56,7 +59,7 @@ class BleService { return false; } } - + /// 获取电池电量 Future getBatteryLevel() async { try { @@ -67,18 +70,19 @@ class BleService { return 0; } } - + /// 获取设备版本信息 Future getDeviceVersion() async { try { - final result = await _methodChannel.invokeMethod('getDeviceVersion'); + final result = + await _methodChannel.invokeMethod('getDeviceVersion'); return result ?? false; } catch (e) { print('获取设备版本失败: $e'); return false; } } - + /// 获取电池信息 Future getBatteryInfo() async { try { @@ -89,7 +93,7 @@ class BleService { return false; } } - + /// 打开编码器 Future openEncoder() async { try { @@ -100,7 +104,7 @@ class BleService { return false; } } - + /// 打开解码器 Future openDecoder() async { try { @@ -111,7 +115,18 @@ class BleService { return false; } } - + + /// 打开解码器 + Future openA2DPDecoder() async { + try { + final result = await _methodChannel.invokeMethod('openA2DPDecoder'); + return result ?? false; + } catch (e) { + print('打开解码器失败: $e'); + return false; + } + } + /// 关闭编解码器 Future closeCodec() async { try { @@ -122,7 +137,7 @@ class BleService { return false; } } - + /// 播放A2DP Future playA2DP() async { try { @@ -133,7 +148,7 @@ class BleService { return false; } } - + /// 启动BLE后台服务 /// [config] 配置参数,可包含: /// - scan_interval: 扫描间隔时间(毫秒) @@ -144,14 +159,15 @@ class BleService { /// - device_name: 设备名称 Future startBleService(Map config) async { try { - final result = await _methodChannel.invokeMethod('startBleService', config); + final result = + await _methodChannel.invokeMethod('startBleService', config); return result ?? false; } catch (e) { print('启动BLE后台服务失败: $e'); return false; } } - + /// 停止BLE后台服务 Future stopBleService() async { try { @@ -163,26 +179,93 @@ class BleService { } } - /// 注册BLE后台扫描 + //======================================================================== + // 以下是Companion Device相关功能 + //======================================================================== + + /// 检查设备是否支持Companion Device Manager /// + /// 返回设备是否支持Companion Device Manager API + /// 仅在Android 8.0及以上版本可用 + Future isCompanionSupported() async { + try { + final result = await _methodChannel.invokeMethod('isCompanionSupported'); + return result ?? false; + } catch (e) { + print('检查Companion设备支持失败: $e'); + return false; + } + } + + /// 启动设备关联流程 + /// + /// 弹出系统设备选择对话框,用户选择后将建立持久化关联 + /// 关联成功后,应用将获得后台扫描和启动前台服务的特权 + /// + /// [deviceNamePattern] 设备名称正则表达式,例如"MyHeadset.*" + /// [serviceUuid] 可选的服务UUID + /// [macAddress] 可选的MAC地址 + /// + /// 返回是否成功启动关联流程 + Future startDeviceAssociation({ + String? deviceNamePattern, + String? serviceUuid, + String? macAddress, + }) async { + try { + final result = await _methodChannel.invokeMethod( + 'startDeviceAssociation', + { + 'deviceNamePattern': deviceNamePattern, + 'serviceUuid': serviceUuid, + 'macAddress': macAddress, + }, + ); + return result ?? false; + } catch (e) { + print('启动设备关联失败: $e'); + return false; + } + } + + /// 获取已关联的设备MAC地址 + /// + /// 返回已关联的设备MAC地址,如果没有则返回null + Future getPairedMacAddress() async { + try { + final result = await _methodChannel.invokeMethod('getPairedMacAddress'); + return result; + } catch (e) { + print('获取配对MAC地址失败: $e'); + return null; + } + } + + + /// 注册BLE后台扫描 + /// /// 注册后,即使应用被杀死,系统也会在检测到设备广播时唤醒应用 /// 这是确保设备可以在应用后台时被自动连接的关键功能 - Future registerBackgroundScan() async { + /// + /// [macAddress] 可选的设备MAC地址,如果不提供则使用已关联的设备 + Future registerBackgroundScan({String? macAddress}) async { try { - final result = await _methodChannel.invokeMethod('registerBackgroundScan'); + final result = + await _methodChannel.invokeMethod('registerBackgroundScan'); return result ?? false; } catch (e) { print('注册后台扫描失败: $e'); return false; } } - + /// 注销BLE后台扫描 - /// + /// /// 当不再需要后台扫描功能时调用,例如用户主动断开连接时 Future unregisterBackgroundScan() async { try { - final result = await _methodChannel.invokeMethod('unregisterBackgroundScan'); + final result = + await _methodChannel.invokeMethod('unregisterBackgroundScan'); return result ?? false; } catch (e) { print('注销后台扫描失败: $e'); @@ -190,12 +273,25 @@ class BleService { } } + /// 清除所有设备关联 + /// + /// 用于重置设备关联状态,通常在用户主动解除关联或调试时使用 + Future clearAssociations() async { + try { + final result = await _methodChannel.invokeMethod('clearAssociations'); + return result ?? false; + } catch (e) { + print('清除设备关联失败: $e'); + return false; + } + } + //======================================================================== // 以下是ChatAgent相关功能 //======================================================================== - + /// 初始化语音交互代理 - /// + /// /// [config] 配置参数,必须包含: /// - azureSpeechKey: Azure语音服务API密钥 /// - azureSpeechRegion: Azure语音服务区域 @@ -204,16 +300,17 @@ class BleService { /// - openaiModel: OpenAI模型名称,默认为'gpt-3.5-turbo' Future initializeChatAgent(Map config) async { try { - final result = await _methodChannel.invokeMethod('initializeChatAgent', config); + final result = await _methodChannel.invokeMethod( + 'initializeChatAgent', config); return result ?? false; } catch (e) { print('初始化语音交互代理失败: $e'); return false; } } - + /// 启动语音识别 - /// + /// /// 用于手动启动语音识别,通常不需要直接调用,因为在唤醒时会自动启动 Future startListening() async { try { @@ -224,7 +321,7 @@ class BleService { return false; } } - + /// 停止语音识别 Future stopListening() async { try { @@ -235,14 +332,16 @@ class BleService { return false; } } - + /// 处理文本输入 - /// + /// /// [text] 用户输入的文本 /// [speakResponse] 是否使用TTS朗读回复,默认为false - Future processTextInput(String text, {bool speakResponse = false}) async { + Future processTextInput(String text, + {bool speakResponse = false}) async { try { - final result = await _methodChannel.invokeMethod('processTextInput', { + final result = + await _methodChannel.invokeMethod('processTextInput', { 'text': text, 'speakResponse': speakResponse, }); @@ -252,7 +351,7 @@ class BleService { return false; } } - + /// 停止TTS播放 Future stopTts() async { try { @@ -263,4 +362,4 @@ class BleService { return false; } } -} \ No newline at end of file +} diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index 038025181..0e3d34c3f 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -20,13 +20,18 @@ class MusicService : Service() { private const val NOTIFICATION_ID = 1002 private val isRunning = AtomicBoolean(false) + private var currentStatus: Boolean = false + private var currentUrlInternal: String? = null + + fun isServiceRunning(): Boolean = isRunning.get() + fun isPlaying(): Boolean = currentStatus + fun getCurrentUrl(): String? = currentUrlInternal } private var mediaPlayer: MediaPlayer? = null private val playlist = mutableListOf() // 可选:初始化为预置歌曲 private var currentIndex = 0 - private var isPlaying = false override fun onCreate() { super.onCreate() @@ -40,7 +45,7 @@ class MusicService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY - Log.i(TAG, "onStartCommand: $command") + Log.i(TAG, "onStartCommand: $command" ) when (command) { "play" -> { @@ -49,6 +54,18 @@ class MusicService : Service() { currentIndex = playlist.indexOf(url) play(url) } + "playlist" -> { + val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY + Log.i(TAG, "后台服务播放 playlist: $urls" ) + if (urls.isNotEmpty()) { + playlist.clear() + playlist.addAll(urls) + currentIndex = 0 + play(playlist[currentIndex]) + } else { + Log.w(TAG, "playlist 命令收到空列表") + } + } "pause" -> pause() "resume" -> resume() "stop" -> stopSelf() @@ -76,24 +93,31 @@ class MusicService : Service() { private fun play(url: String) { Log.i(TAG, "播放音乐: $url") releaseMediaPlayer() - + currentStatus = true; + currentUrlInternal = url; mediaPlayer = MediaPlayer().apply { setDataSource(url) setAudioStreamType(AudioManager.STREAM_MUSIC) setOnPreparedListener { start() - this@MusicService.isPlaying = true + currentStatus = true updateNotification("正在播放") Log.i(TAG, "开始播放") } setOnCompletionListener { Log.i(TAG, "播放完成") - this@MusicService.isPlaying = false - stopSelf() + // 播放下一首 + if (currentIndex + 1 < playlist.size) { + currentIndex++ + play(playlist[currentIndex]) // 播放下一首 + } else { + currentStatus = false + stopSelf() // 如果没有更多歌曲了,可以停止服务 + } } setOnErrorListener { _, what, extra -> Log.e(TAG, "播放错误: what=$what extra=$extra") - this@MusicService.isPlaying = false + currentStatus = false stopSelf() true } @@ -105,7 +129,7 @@ class MusicService : Service() { mediaPlayer?.let { if (it.isPlaying) { it.pause() - this@MusicService.isPlaying = false + currentStatus= false updateNotification("已暂停") Log.i(TAG, "暂停播放") } @@ -116,7 +140,7 @@ class MusicService : Service() { mediaPlayer?.let { if (!it.isPlaying) { it.start() - this@MusicService.isPlaying = true + currentStatus = true updateNotification("继续播放") Log.i(TAG, "继续播放") } diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt index 6741e7937..f7686faf2 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt @@ -18,7 +18,7 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { context = flutterPluginBinding.applicationContext - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "music_service") + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.music_service") channel.setMethodCallHandler(this) } @@ -30,6 +30,10 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { val url = call.argument("url") MusicServiceStarter.startServiceWithCommand(context, "play", url) } + "playlist" -> { + val urls = call.argument>("urls") + MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls) + } "pause" -> MusicServiceStarter.startServiceWithCommand(context, "pause") "resume" -> MusicServiceStarter.startServiceWithCommand(context, "resume") "stop" -> MusicServiceStarter.startServiceWithCommand(context, "stop") @@ -37,6 +41,15 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler { val volume = call.argument("volume")?.toFloat() MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume) } + "getStatus" -> { + val isPlaying = MusicService.isPlaying() + val currentUrl = MusicService.getCurrentUrl() + val status = mapOf( + "isPlaying" to isPlaying, + "url" to currentUrl + ) + result.success(status) + } else -> result.notImplemented() } result.success(null) diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt index dbcd05cca..9d8d290a1 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt @@ -7,6 +7,7 @@ import androidx.core.content.ContextCompat object MusicServiceStarter { private val TAG = "MusicServiceStarter" + //播放单曲 fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) { Log.i(TAG, "startServiceWithCommand: $command $url") val intent = Intent(context, MusicService::class.java).apply { @@ -16,4 +17,14 @@ object MusicServiceStarter { } ContextCompat.startForegroundService(context, intent) } + + //启动播放列表 + fun startServiceWithPlaylist(context: Context,command: String, urls: List?,volume: Float? = null) { + Log.i(TAG, "startServiceWithPlaylist: $urls") + val intent = Intent(context, MusicService::class.java).apply { + putExtra("command", command) + putStringArrayListExtra("urls", ArrayList(urls!!)) // 注意类型是 ArrayList + } + ContextCompat.startForegroundService(context, intent) + } } \ No newline at end of file diff --git a/local_plugins/music_service/lib/music_service.dart b/local_plugins/music_service/lib/music_service.dart index cace4292f..ce3bb9e6d 100644 --- a/local_plugins/music_service/lib/music_service.dart +++ b/local_plugins/music_service/lib/music_service.dart @@ -52,4 +52,11 @@ class MusicService { 'setVolume', {'volume': volume}, ); // volume: 0.0~1.0 + Future> getStatus() async { + final result = await _methodChannel.invokeMethod('getStatus'); + return { + 'isPlaying': result?['isPlaying'] ?? false, + 'url': result?['url'] ?? "", + }; + } } diff --git a/local_plugins/music_service/pubspec.yaml b/local_plugins/music_service/pubspec.yaml index 28633c08c..47ed9d37e 100644 --- a/local_plugins/music_service/pubspec.yaml +++ b/local_plugins/music_service/pubspec.yaml @@ -4,7 +4,7 @@ version: 0.0.1 homepage: environment: - sdk: ^3.7.2 + sdk: '>=3.7.0 <4.0.0' flutter: '>=3.3.0' dependencies: diff --git a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt index 9d3a68ed0..a94f7f627 100644 --- a/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt +++ b/local_plugins/open_ai_service/android/src/main/kotlin/com/yunqiinnovation/open_ai_service/OpenAIService.kt @@ -338,29 +338,46 @@ class OpenAIService(private val context: Context? = null) { // 获取参数 val argumentsJson = functionCall.getString("arguments") - Log.e(TAG, "自动处理MCP工具调用: name:${name} argumentsJson:${argumentsJson}") + Log.d(TAG, "自动处理MCP工具调用: name=${name}, arguments=${argumentsJson}") val arguments = mcpClient?.parseJsonArguments(argumentsJson) ?: mapOf() // 调用工具(统一通过MCPClient处理) val result = mcpClient?.callTool(name, arguments) - callback.onFunctionCallResult(functionCall, result?: JSONObject().apply { put("context","工具调用失败") }) - // 不论是本地函数还是MCP工具,都直接将结果发送回OpenAI + + // 检查是否已取消 + if (isCanceled) { + Log.d(TAG, "工具调用已被取消,不处理结果") + return + } + + // 回调结果 + val resultObj = result ?: JSONObject().apply { put("context", "工具调用失败") } + callback.onFunctionCallResult(functionCall, resultObj) + + // 发送结果到OpenAI继续对话 + val resultContent = if (resultObj.has("context") && resultObj.getString("context").isNotEmpty()) { + resultObj.getString("context") + } else { + "工具调用失败" + } sendFunctionCallResult( messages = messages, functionCall = functionCall, - functionResult = result?.getString("context")?: "工具调用失败", + functionResult = resultContent, callback = callback ) } catch (e: Exception) { - Log.e(TAG, "自动处理工具调用失败: ${e.message}", e) - // 失败时返回错误给回调函数 - val errorMessage = "工具调用失败: ${e.message}" - sendFunctionCallResult( - messages = messages, - functionCall = functionCall, - functionResult = errorMessage, - callback = callback - ) + if (!isCanceled) { + Log.e(TAG, "自动处理工具调用失败: ${e.message}", e) + // 失败时返回错误给回调函数 + val errorMessage = "工具调用失败: ${e.message}" + sendFunctionCallResult( + messages = messages, + functionCall = functionCall, + functionResult = errorMessage, + callback = callback + ) + } } } @@ -554,7 +571,6 @@ class OpenAIService(private val context: Context? = null) { try { // 用于存储函数调用的各个部分 val finalToolCalls = mutableMapOf() - val lineBuffer = StringBuilder() // 逐块读取数据,而不是一次性预取 val BUFFER_SIZE = 8192L // 8KB的块大小 @@ -571,14 +587,11 @@ class OpenAIService(private val context: Context? = null) { if (line.startsWith("data:")) { val data = line.substring(5).trim() - // Log.d("OpenAIService", "$data") // 处理[DONE]消息 if (data == "[DONE]" || data == "[\"DONE\"]") { val hasToolCalls = processToolCalls(finalToolCalls, callback, messages) - // Log.d(TAG, "收到[DONE]消息, $hasToolCalls") - - // 只有在没有工具调用时才认为对话真正完成 + // 如果没有工具调用,这是对话的真正结束 if (!hasToolCalls) { callback.onComplete() } @@ -594,22 +607,25 @@ class OpenAIService(private val context: Context? = null) { if (choices.length() > 0) { val choice = choices.getJSONObject(0) + // 处理delta内容 if (choice.has("delta")) { val delta = choice.getJSONObject("delta") // 处理普通文本内容 if (delta.has("content")) { val content = delta.getString("content") - callback.onToken(content) + if (!isCanceled) { + callback.onToken(content) + } } - // 处理工具调用(函数调用) + // 处理工具调用 if (delta.has("tool_calls")) { val toolCalls = delta.getJSONArray("tool_calls") for (i in 0 until toolCalls.length()) { val toolCall = toolCalls.getJSONObject(i) val index = toolCall.getInt("index") - // Log.e(TAG, "解析道AI需要调用工具: $toolCall") + // 创建或获取现有的工具调用信息 val toolCallInfo = finalToolCalls.getOrPut(index) { ToolCallInfo() } @@ -633,25 +649,40 @@ class OpenAIService(private val context: Context? = null) { } } } + + // 检查是否有表示完成的标志 + if (choice.has("finish_reason")) { + val finishReason = choice.getString("finish_reason") + if (finishReason == "stop" || finishReason == "length") { + // 正常完成,没有工具调用 + callback.onComplete() + return + } else if (finishReason == "tool_calls") { + // 处理工具调用 + val hasToolCalls = processToolCalls(finalToolCalls, callback, messages) + if (!hasToolCalls) { + callback.onComplete() + } + return + } + } } } } catch (e: Exception) { - // 忽略解析错误 - Log.e(TAG, "解析JSON出错: ${e.message}") + // 忽略解析错误,但记录日志 + Log.e(TAG, "解析JSON出错: ${e.message}", e) } } } } - // 如果是因为取消导致循环结束,不继续处理 + // 如果是因为取消导致循环结束,直接返回 if (isCanceled) { return } - // 处理可能尚未处理的工具调用 + // 如果没有明确的完成信号但数据流结束了,处理最后可能的工具调用 val hasToolCalls = processToolCalls(finalToolCalls, callback, messages) - - // 只有在没有工具调用时才认为对话真正完成 if (!hasToolCalls) { callback.onComplete() } @@ -677,6 +708,11 @@ class OpenAIService(private val context: Context? = null) { functionResult: String, callback: StreamCallback ) { + if (isCanceled) { + Log.d(TAG, "请求已取消,不发送函数调用结果") + return + } + try { val fullMessages = JSONArray() @@ -686,6 +722,7 @@ class OpenAIService(private val context: Context? = null) { } // 添加函数调用消息 + val callId = functionCall.optString("id", "call_${System.currentTimeMillis()}") fullMessages.put(JSONObject().apply { put("role", "assistant") put("content", "") @@ -693,7 +730,7 @@ class OpenAIService(private val context: Context? = null) { // 添加工具调用 val toolCalls = JSONArray().apply { val toolCall = JSONObject().apply { - put("id", functionCall.optString("id", "call_${System.currentTimeMillis()}")) + put("id", callId) put("type", "function") put("function", JSONObject().apply { put("name", functionCall.getString("name")) @@ -709,13 +746,17 @@ class OpenAIService(private val context: Context? = null) { fullMessages.put(JSONObject().apply { put("role", "tool") put("content", functionResult) - put("tool_call_id", functionCall.optString("id", "call_${System.currentTimeMillis()}")) + put("tool_call_id", callId) }) + // 发送完整对话 sendMessageStream(fullMessages, callback) } catch (e: Exception) { - callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}")) + if (!isCanceled) { + Log.e(TAG, "发送函数调用结果失败: ${e.message}", e) + callback.onError(OpenAIException("发送函数调用结果失败: ${e.message}")) + } } } @@ -736,17 +777,35 @@ class OpenAIService(private val context: Context? = null) { put("id", firstToolCall.id) } Log.d(TAG, "工具调用: $functionCall") + + // 通知上层工具调用事件 + callback.onFunctionCall(functionCall) + // 在协程中处理工具调用 if (messages != null) { kotlinx.coroutines.GlobalScope.launch { try { - - //通知上层回调事件,但不关心结果 - callback.onFunctionCall(functionCall) - autoHandleMcpToolCall(functionCall, messages, callback) - + if (!isCanceled) { + autoHandleMcpToolCall(functionCall, messages, callback) + } else { + Log.d(TAG, "请求已取消,不处理工具调用") + } } catch (e: Exception) { - Log.e(TAG, "工具调用处理失败: ${e.message}", e) + if (!isCanceled) { + Log.e(TAG, "处理工具调用时发生异常: ${e.message}", e) + // 处理异常的情况,向回调通知错误 + try { + val errorMessage = "工具调用处理失败: ${e.message}" + sendFunctionCallResult( + messages = messages, + functionCall = functionCall, + functionResult = errorMessage, + callback = callback + ) + } catch (e2: Exception) { + Log.e(TAG, "发送工具调用错误结果失败: ${e2.message}", e2) + } + } } } } diff --git a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock index 666b59b98..ce3086c05 100644 Binary files a/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock and b/local_plugins/opus/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index fafba6f95..118d77df6 100644 Binary files a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties index 06083f70c..3ca77d0f1 100644 --- a/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties +++ b/local_plugins/opus/.gradle/buildOutputCleanup/cache.properties @@ -1,2 +1,2 @@ -#Fri May 09 15:56:53 IST 2025 +#Sat May 10 17:59:51 IST 2025 gradle.version=8.10