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 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..8d8176211 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 @@ -261,7 +261,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..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 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