From 65ff13a0c2d8b82d8e3c2cb9f4c55da48adea6a5 Mon Sep 17 00:00:00 2001 From: wolfplus Date: Sat, 10 May 2025 18:49:27 +0100 Subject: [PATCH] add --- lib/data/services/ble_manager.dart | 3 - .../controllers/pairing_controller.dart | 171 ++++++- .../android/src/main/AndroidManifest.xml | 12 +- .../agent_service/AgentService.kt | 184 +++++-- .../yunqiinnovation/agent_service/BleAgent.kt | 2 +- .../agent_service/BleMediaService.kt | 131 +---- .../agent_service/BleWakeReceiver.kt | 85 ---- .../agent_service/BootReceiver.kt | 19 - .../android/src/main/res/raw/silence.mp3 | Bin .../android/src/main/res/raw/start.mp3 | Bin 0 -> 56721 bytes .../android/src/main/res/raw/stop.mp3 | Bin 0 -> 12898 bytes .../android/src/main/res/raw/stop2.mp3 | Bin 0 -> 23284 bytes .../azure_speech/AzureSpeechPlugin.kt | 2 +- .../android/src/main/AndroidManifest.xml | 32 +- .../ble_service/BleCompanionManager.kt | 481 ++++++++++++++++++ .../ble_service/BleCompanionService.kt | 73 +++ .../yunqiinnovation/ble_service/BleService.kt | 28 + .../ble_service/BleServicePlugin.kt | 123 ++++- .../ble_service/BleWakeReceiver.kt | 69 +++ .../ble_service/BootReceiver.kt | 26 + .../ble_service/lib/ble_service.dart | 84 ++- .../open_ai_service/OpenAIService.kt | 127 +++-- .../.gradle/8.10/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .../buildOutputCleanup.lock | Bin 17 -> 17 bytes .../buildOutputCleanup/cache.properties | 2 +- 25 files changed, 1306 insertions(+), 348 deletions(-) delete mode 100644 local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BleWakeReceiver.kt delete mode 100644 local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/BootReceiver.kt rename local_plugins/{ble_service => agent_service}/android/src/main/res/raw/silence.mp3 (100%) create mode 100644 local_plugins/agent_service/android/src/main/res/raw/start.mp3 create mode 100644 local_plugins/agent_service/android/src/main/res/raw/stop.mp3 create mode 100644 local_plugins/agent_service/android/src/main/res/raw/stop2.mp3 create mode 100644 local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionManager.kt create mode 100644 local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleCompanionService.kt create mode 100644 local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BleWakeReceiver.kt create mode 100644 local_plugins/ble_service/android/src/main/kotlin/com/yunqiinnovation/ble_service/BootReceiver.kt diff --git a/lib/data/services/ble_manager.dart b/lib/data/services/ble_manager.dart index ce8302c92..078d06176 100644 --- a/lib/data/services/ble_manager.dart +++ b/lib/data/services/ble_manager.dart @@ -483,9 +483,6 @@ class BleManager extends GetxService { } } - registerBackgroundScan() { - _bleService.registerBackgroundScan(); - } /// 获取电池电量 Future getBatteryLevel() async { 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/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 35ed506ed..d5e696c8a 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 @@ -13,6 +13,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 /** * 代理服务事件监听接口 */ @@ -68,6 +70,9 @@ object AgentService : CoroutineScope { // 系统提示词 private var systemPrompt = "" + // 音频播放器 + private var audioPlayer: AudioPlayer? = null // 初始化音频播放器 + // 状态 private var isInitialized = false var isRecognitionActive = false @@ -120,6 +125,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 ?: "" @@ -353,12 +360,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) { @@ -438,24 +447,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 + } } } @@ -657,21 +667,30 @@ 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) } } override fun onFunctionCallResult(functionCall: JSONObject, functionCallResult: JSONObject) { - sendEvent("function_call_result", mapOf( - "function_call" to functionCall.toString(), - "result" to functionCallResult.toString(), - )) - FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + try { + sendEvent("function_call_result", mapOf( + "function_call" to functionCall.toString(), + "result" to functionCallResult.toString() + )) + FileLogger.d(TAG, "mcp调用结果: $functionCallResult") + } catch (e: Exception) { + FileLogger.e(TAG, "处理函数调用结果事件失败", e) + } } } ) @@ -850,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) + } } /** @@ -948,4 +985,59 @@ object AgentService : CoroutineScope { return true } + + /** + * 简单音频播放器,用于播放提示音等 + */ + 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/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..88187eec9 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 @@ -70,7 +70,6 @@ class BleMediaService : Service(), CoroutineScope { private lateinit var mediaSession: MediaSessionCompat private lateinit var audioManager: AudioManager - private var isPlaying = false // 媒体操作防抖 @@ -94,7 +93,7 @@ class BleMediaService : Service(), CoroutineScope { // 设置服务状态 isRunning.set(true) - + startForeground(NOTIFICATION_ID, createNotification()) // 初始化音频管理器 audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager @@ -102,23 +101,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 +130,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 } @@ -297,24 +287,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 +321,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 +411,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 0000000000000000000000000000000000000000..b902924465f7ecf006c61fbf09b873cc1c8152fb GIT binary patch literal 56721 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 ld*t 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 { 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..9f52b5e55 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地址 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..d9c8faad8 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) } @@ -160,7 +206,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 +275,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 +320,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..323083350 100644 --- a/local_plugins/ble_service/lib/ble_service.dart +++ b/local_plugins/ble_service/lib/ble_service.dart @@ -163,13 +163,80 @@ class BleService { } } + //======================================================================== + // 以下是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', + {'macAddress': macAddress}, + ); return result ?? false; } catch (e) { print('注册后台扫描失败: $e'); @@ -190,6 +257,19 @@ class BleService { } } + /// 清除所有设备关联 + /// + /// 用于重置设备关联状态,通常在用户主动解除关联或调试时使用 + Future clearAssociations() async { + try { + final result = await _methodChannel.invokeMethod('clearAssociations'); + return result ?? false; + } catch (e) { + print('清除设备关联失败: $e'); + return false; + } + } + //======================================================================== // 以下是ChatAgent相关功能 //======================================================================== 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..dbc1af023 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,42 @@ 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 = resultObj.optString("context", "工具调用失败") 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 +567,6 @@ class OpenAIService(private val context: Context? = null) { try { // 用于存储函数调用的各个部分 val finalToolCalls = mutableMapOf() - val lineBuffer = StringBuilder() // 逐块读取数据,而不是一次性预取 val BUFFER_SIZE = 8192L // 8KB的块大小 @@ -571,14 +583,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 +603,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 +645,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 +704,11 @@ class OpenAIService(private val context: Context? = null) { functionResult: String, callback: StreamCallback ) { + if (isCanceled) { + Log.d(TAG, "请求已取消,不发送函数调用结果") + return + } + try { val fullMessages = JSONArray() @@ -686,6 +718,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 +726,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 +742,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 +773,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 666b59b98c85050cdf304292b694f6a135bfece4..ce3086c05dc7c6515bdf4f11ac8f3f79e344082b 100644 GIT binary patch literal 17 UcmZQRo$b{-r|q*H0|Y1l05?noga7~l literal 17 UcmZQRo$b{-r|q*H0|dwc05?hmf&c&j diff --git a/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/local_plugins/opus/.gradle/buildOutputCleanup/buildOutputCleanup.lock index fafba6f95c378aa6a0e908a1943ef52331b26ead..118d77df6ef1b4541bbd050c5f11b7312c67319c 100644 GIT binary patch literal 17 TcmZQ}Dp$CADo{Lz0RprDDJ=tP literal 17 TcmZQ}Dp$CADo{Lz0Rl7tDJuhL 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