From 552406417f3a83554a962fd005af208027263c9d Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 19 Sep 2025 10:57:00 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0qq=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E6=92=AD=E6=94=BE=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/yunqiinnovation/qq_music/QQMusicLoginManager.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt index ca0969865..a9474d997 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt @@ -149,7 +149,12 @@ class QQMusicLoginManager(private val context: Context) { Log.d(TAG, "启动QQ音乐授权流程") // 启动QQ音乐登录 - CommonCmd.startQQMusicProcess(context,context.packageName) +// CommonCmd.startQQMusicProcess(context,context.packageName) + CommonCmd.loginQQMusic( + context, + context.packageName, + BASE_SCHEME + ) callback.onSuccess() // 等待用户登录完成后进行身份验证 // handler.postDelayed({ From c3c6d0552f32c10d1dd41d973ecbbcabbe3d5b4e Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 19 Sep 2025 15:40:15 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dtts=E5=A5=94=E6=BA=83?= =?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=8C=E5=85=81=E8=AE=B8=E6=B7=B7=E9=9F=B3?= =?UTF-8?q?=E3=80=829.19?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/azure_speech/AzureTtsHelper.swift | 27 ++++- .../Sources/tools/MicrophoneCapture.swift | 24 ----- .../Sources/tools/SimpleAudioReceiver.swift | 99 +++---------------- 3 files changed, 36 insertions(+), 114 deletions(-) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift index 30dfd7464..a5857fe4e 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/azure_speech/AzureTtsHelper.swift @@ -124,10 +124,33 @@ public class AzureTtsHelper: NSObject, ITtsService { */ public func startspeak(sessionid sessionid: String)-> Bool { self.sessionid = sessionid - + print("startspeak sessionid \(sessionid)") // 重置当前会话的文本计数 self.currentSessionTextCount = 0 self.pendingTextCount = 0 + + do { + // 新增:使用 AVAudioSession.sharedInstance 配置为“通话模式 + 扬声器外放” + // - 使用 .playAndRecord 以保证通话上下行链路 + // - 使用 .voiceChat 获得通话处理(AEC 等) + // - 使用 .defaultToSpeaker 默认走外放 + // - 通过 overrideOutputAudioPort(.speaker) 强制切到扬声器 + let audioSession = AVAudioSession.sharedInstance() + + //try audioSession.setActive(false) + try audioSession.setCategory(.playAndRecord, + mode: .voiceChat, + options: [.defaultToSpeaker, + .mixWithOthers, + .defaultToSpeaker, + .allowBluetoothA2DP, + .allowBluetooth]) + try audioSession.overrideOutputAudioPort(.none) + try audioSession.setActive(true) + } catch { + print("音频会话配置失败: \(error.localizedDescription)") + } + return true } @@ -187,7 +210,7 @@ public class AzureTtsHelper: NSObject, ITtsService { totalTextCount += text.count currentSessionTextCount += text.count - print("流式合成文本\(text.count),总计文本数量:\(totalTextCount),当前会话:\(currentSessionTextCount)") + //print("流式合成文本\(text.count),总计文本数量:\(totalTextCount),当前会话:\(currentSessionTextCount)") // 清理文本并添加到缓冲区 let cleanedText = cleanTextForTTS(text) diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift index 857d30041..1d14da379 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/MicrophoneCapture.swift @@ -445,30 +445,6 @@ public class MicrophoneCapture: NSObject { // 安全地处理音频会话 guard let audioSession = audioSession else { return } - do { - try audioSession.setActive(false) - - if self.hasBluetoothDevices { - // 设置音频会话参数 - try audioSession.setCategory(.playback, - mode: .videoChat, - options: [ - .allowBluetoothA2DP, - .allowBluetooth - ]) // 添加音频优先级控制 - } else { - try audioSession.setCategory(.playback, - mode: .videoChat, - options: [ - .mixWithOthers, - .defaultToSpeaker]) - } - - try audioSession.overrideOutputAudioPort(.none) - try audioSession.setActive(true) - } catch { - print("音频会话配置失败: \(error.localizedDescription)") - } } /** diff --git a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift index 5aef34be7..b37cd56f4 100644 --- a/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift +++ b/local_plugins/azure_speech/ios/azure_speech/Sources/tools/SimpleAudioReceiver.swift @@ -193,12 +193,15 @@ public class SimpleAudioReceiver: NSObject { // 确保正在写入状态 guard self._isWriting else { return } // 设置音频会话参数 - try audioSession.setCategory(.playAndRecord, - mode: .videoChat, - options: [.allowBluetoothA2DP, // 允许蓝牙耳机,不占用hfp链路 - .mixWithOthers, - .defaultToSpeaker]) // 添加音频优先级控制 - try audioSession.setActive(true) + try audioSession.setCategory(.playAndRecord, + mode: .voiceChat, + options: [.defaultToSpeaker, + .mixWithOthers, + .defaultToSpeaker, + .allowBluetoothA2DP, + .allowBluetooth]) + try audioSession.overrideOutputAudioPort(.none) + try audioSession.setActive(true) try micCapture.startCapture() try micCapture.audioDataHandler = { data in self.writeQueue.put(data) @@ -264,37 +267,7 @@ public class SimpleAudioReceiver: NSObject { * 强制使用设备内置扬声器或听筒 */ public func disableBluetoothAudio() { - // do { - // print("禁用蓝牙音频功能,切换回正常音频模式") - - // // 暂停当前音频引擎 - // if _isWriting { - // audioEngine?.pause() - // try audioSession.setActive(false) - // } - - // // 设置音频会话为不允许蓝牙 - // try audioSession.setCategory( - // .playAndRecord, - // mode: .videoChat, - // options: [.defaultToSpeaker, .mixWithOthers] // 移除 .allowBluetooth - // ) - - // // 强制使用扬声器 - // try audioSession.overrideOutputAudioPort(.speaker) - - // // 重新激活音频会话 - // if _isWriting { - // try audioSession.setActive(true) - // try audioEngine?.start() - // } - - // // 更新当前路由状态 - // currentRoute = .speaker - - // } catch { - // print("禁用蓝牙音频失败: \(error.localizedDescription)") - // } + } /** @@ -369,57 +342,7 @@ public func restoreOriginalAudioState() { public func setAudioOutputRoute(_ route: AudioOutputRoute) { - // do { - // // - // if self._isWriting { - // print("当前正在识别") - // micCapture.stopCapture() - // // 先停用以避免冲突 - // try audioSession.setActive(false) - - // } - // print("setAudioOutputRoutecurrent,route=\(route)") - // switch route { - // case .speaker: - // print("setAudioOutputRoutecurrent,speaker") - // // 使用扬声器时必须用videoChat模式 - // try audioSession.setCategory( - // .playAndRecord, - // mode: .videoChat, - // options: [.defaultToSpeaker,.allowBluetooth,.mixWithOthers] - // ) - // try audioSession.overrideOutputAudioPort(.speaker) - - // case .receiver: - // // 听筒模式使用voiceChat节省资源 - // try audioSession.setCategory( - // .playAndRecord, - // mode: .voiceChat, - // options: [.allowBluetooth] - // ) - // try audioSession.overrideOutputAudioPort(.none) - - // case .bluetooth: - // // 完整蓝牙设备支持 - // try audioSession.setCategory( - // .playAndRecord, - // mode: .voiceChat, - // options: [.allowBluetooth, .allowBluetoothA2DP] - // ) - // try audioSession.overrideOutputAudioPort(.none) - // // 不需要override,系统自动路由 - // } - // self.currentRoute = route - // if self._isWriting { - - // // 重新激活 - // try audioSession.setActive(true, options: [.notifyOthersOnDeactivation]) - // try micCapture.startCapture() - - // } - // } catch { - // print("路由切换失败: \(error)") - // } + } From 12a36a5fe7bd0f62d6eb55f3496d6f8d7c214f3f Mon Sep 17 00:00:00 2001 From: fdp <1286779656@qq.com> Date: Fri, 19 Sep 2025 16:30:08 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E6=89=93=E7=94=B5=E8=AF=9D=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E5=8E=BB=E5=A4=84=E5=98=9F=E5=98=9F=E5=98=9F=E5=A3=B0?= =?UTF-8?q?=E9=9F=B3=EF=BC=8C=E5=BD=B1=E5=93=8D=E5=9B=9E=E9=9F=B3=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/agent_service/AgentServiceImpl.swift | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index b294b6a05..e51d45a41 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -81,7 +81,7 @@ class AgentServiceImpl: NSObject { private let maxIdleSeconds: TimeInterval = 10 var micCapture: MicrophoneCapture! // 语音识别模式 - private var currentRecognitionMode: String = "normal" + public var currentRecognitionMode: String = "normal" internal var audioPlayer: AudioPlayer? // private var locationInfo:[String: String]? @@ -1581,9 +1581,14 @@ class AudioPlayer { } func playAwaitSound() { - // 先停止之前的等待音效和定时器 + //先停止之前的等待音效和定时器 stopAwaitSound() + // 修复:通过 agentService 引用访问 currentRecognitionMode + if agentService?.currentRecognitionMode == "phone_call" {//电话模式不要嘟嘟嘟。影响回音消除功能 + return + } + // 播放等待音效 playLoopSound(named: "await", fileType: "mp3", player: &awaitPlayer,volume:0.2) From 1a131ec5f37a3db58de78c0747bd06a9ae21920f Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 19 Sep 2025 19:34:30 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0qq=E9=9F=B3=E4=B9=90?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ios/Runner/Info.plist | 3 + .../chat_api/QQMusicSystemFunctionHandler.kt | 87 +++++++------------ .../chat_api/MusicSystemFunctionHandler.swift | 1 + .../QQMusicSystemFunctionHandler.swift | 1 + .../qq_music/QQMusicLoginManager.kt | 70 ++++++++++----- .../qq_music/QQMusicSingleton.kt | 3 +- 6 files changed, 87 insertions(+), 78 deletions(-) diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 1087023b6..ec7521c06 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -91,6 +91,9 @@ google.navigation googlemaps comgooglemaps + baidumap + amapuri + maps LSRequiresIPhoneOS diff --git a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt index db3684402..a820c23d3 100644 --- a/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt +++ b/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt @@ -124,7 +124,26 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand qqMusicSingleton.searchSongs(keyword) { searchResult -> val result = if (searchResult.isSuccess) { val songs = searchResult.getOrNull() ?: emptyList() - + var playError: Throwable? = null + if (songs.isNotEmpty()) { + // 从搜索结果中提取歌曲MID列表 + val songMids = songs.mapNotNull { song -> + val songMap = song as Map + songMap["mid"] as? String + }.filter { it.isNotEmpty() } + + if (songMids.isNotEmpty()) { + // 播放搜索到的歌曲列表,从第一首开始播放 + qqMusicSingleton.playSongs(songMids, 0) { playResult -> + Log.d("QQMusicSearchHandler", "播放结果: ${playResult.isSuccess}") + if (!playResult.isSuccess) { + playError = playResult.exceptionOrNull() +// Log.e("QQMusicSearchHandler", "播放失败: ${playError?.message}") + } + } + } + } + // 根据搜索类型处理结果 val processedSongs = when (searchType) { 0 -> { // 歌曲搜索,返回所有结果 @@ -148,7 +167,18 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand val duration = songMap["duration"] ?: 0 "{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" } - "{\"success\": true,\"iscard\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}" + + if (songs.isNotEmpty()) { + if (playError == null){ + "{\"success\": true, \"iscard\": true, \"broadcast\": false, \"message\": \"搜索成功,正在播放\", \"data\": [$songsJson]}" + }else{ + Log.e("QQMusicSearchHandler", "播放失败: ${playError?.message}") + "{\"success\": false, \"iscard\": true, \"broadcast\": true, \"message\": \"${playError?.message}\", \"data\": [$songsJson]}" + } + + } else { + "{\"success\": false, \"message\": \"未找到相关歌曲\"}" + } } else { val error = searchResult.exceptionOrNull() "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" @@ -163,59 +193,6 @@ private class QQMusicSearchHandler(private val context: Context?) : FunctionHand } } } -//private class QQMusicSearchHandler(private val context: Context?) : FunctionHandler { -// override suspend fun handle(arguments: Map): String { -// return try { -// val keyword = arguments["keyword"] as? String ?: return "{\"success\": false, \"message\": \"缺少搜索关键词\"}" -// val searchType = arguments["searchType"] as? Int ?: 0 // 默认搜索歌曲 -// val qqMusicSingleton = QQMusicSingleton.getInstance() -// var result = "" -// -// // 直接调用搜索,内部会自动处理登录检查 -// qqMusicSingleton.searchSongs(keyword) { searchResult -> -// result = if (searchResult.isSuccess) { -// val songs = searchResult.getOrNull() ?: emptyList() -// -// // 根据搜索类型处理结果 -// val processedSongs = when (searchType) { -// 0 -> { // 歌曲搜索,只返回第一个结果 -//// if (songs.isNotEmpty()) listOf(songs.first()) else emptyList() -// songs -// } -// else -> { // 专辑、歌手等其他类型,返回所有结果 -// songs -// } -// } -// -// val songsJson = processedSongs.joinToString(",") { song -> -// val songMap = song as Map -// // 添加调试日志 -// Log.d("QQMusicSearchHandler", "原始数据: $songMap") -// // 修正字段名映射 -// val mid = songMap["mid"] ?: "unknown" -// val name = songMap["name"] ?: "unknown" -// val artist = songMap["singer"] ?: "unknown" -// val album = songMap["album"] ?: "" -// val pic = songMap["pic"] ?: "" -// val duration = songMap["duration"] ?: 0 -// "{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" -// } -// "{\"success\": true, \"broadcast\": false, \"message\": \"搜索成功,增在播放\", \"data\": [$songsJson]}" -// } else { -// val error = searchResult.exceptionOrNull() -// "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" -// } -// } -// -// // 等待回调完成 -// delay(5000) -// result.ifEmpty { "{\"success\": false, \"message\": \"搜索超时\"}" } -// } catch (e: Exception) { -// Log.e("QQMusicSearchHandler", "QQ音乐搜索失败", e) -// "{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}" -// } -// } -//} private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionHandler { override suspend fun handle(arguments: Map): String { diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift index c2e2c3409..9d65f4fdd 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MusicSystemFunctionHandler.swift @@ -67,6 +67,7 @@ private class MusicSearchHandler: FunctionHandler { let result: [String: Any] = [ "success": true, "broadcast": false, + "iscard": true, "message": "搜索成功,正在播放", "data": songsJson ] diff --git a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift index c8c3144aa..2558ca63b 100644 --- a/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift +++ b/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift @@ -162,6 +162,7 @@ private class QQMusicSearchHandler: FunctionHandler { result = [ "success": true, "message": "Found \(items.count) songs", + "iscard": true, "broadcast": false, "data": songs ] diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt index a9474d997..396c6cf76 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt @@ -1,6 +1,7 @@ package com.yunqiinnovation.qq_music import android.app.Activity +import android.app.ActivityManager import android.content.Context import android.os.Bundle import android.os.Handler @@ -18,12 +19,12 @@ import org.json.JSONTokener * 负责处理QQ音乐的登录授权流程 */ class QQMusicLoginManager(private val context: Context) { - + companion object { private const val TAG = "QQMusicLoginManager" private const val BASE_SCHEME = "voitrans://" private const val URI_LOGIN = BASE_SCHEME + "login" - + // 定义错误码常量,替代 LoginExampleActivity 中的常量 const val LOGIN_FAILED = 1 } @@ -32,7 +33,7 @@ class QQMusicLoginManager(private val context: Context) { private var qqMusicApi: IQQMusicApi? = null private lateinit var qqMusicApiWrapper: QQMusicApiWrapper private val handler = Handler(Looper.getMainLooper()) - + /** * 登录结果回调接口 */ @@ -51,57 +52,57 @@ class QQMusicLoginManager(private val context: Context) { */ fun startLogin(callback: LoginCallback) { Log.d(TAG, "开始QQ音乐登录流程") - + // 检查配置 if (Config.OPENID_APPID.isEmpty() || Config.OPENID_APP_PRIVATE_KEY.isEmpty()) { callback.onError("CONFIG_ERROR", "请先配置APPID和私钥") return } - + // 初始化绑定助手 qPlayBindHelper = QPlayBindHelper(context) - + // 开始绑定QQ音乐服务 bindQQMusicService(callback) } - + /** * 绑定QQ音乐服务 */ private fun bindQQMusicService(callback: LoginCallback) { Log.d(TAG, "开始绑定QQ音乐服务") - + qPlayBindHelper.ensureQQMusicBindByStartProcess { success -> if (success && qPlayBindHelper.isBindQQMusic()) { Log.d(TAG, "QQ音乐服务绑定成功") qqMusicApi = qPlayBindHelper.getQQMusicApi()!! qqMusicApiWrapper = QQMusicApiWrapper(qqMusicApi!!) - + // 检查是否已经授权 checkAuthStatus(callback) - + } else { Log.e(TAG, "QQ音乐服务绑定失败") callback.onError("APP_NOT_INSTALLED", "请先安装QQ音乐App才能使用音乐功能") } } } - + /** * 检查授权状态 */ private fun checkAuthStatus(callback: LoginCallback) { Log.d(TAG, "检查授权状态") - + val bundle = Bundle().apply { putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) } - + try { val result = qqMusicApi?.execute("hi", bundle) val code = result?.getInt(Keys.API_RETURN_KEY_CODE) - + when (code) { ErrorCodes.ERROR_OK -> { // 已授权,直接请求登录信息 @@ -147,21 +148,46 @@ class QQMusicLoginManager(private val context: Context) { */ fun startAuthProcess2(callback: LoginCallback) { Log.d(TAG, "启动QQ音乐授权流程") - + if (isAppInForeground(context)) { + CommonCmd.loginQQMusic( + context, + context.packageName, + BASE_SCHEME + ) +// Log.d(TAG, "当前是前台服务") + } else { + // CommonCmd.loginQQMusic( + // context, + // context.packageName, + // BASE_SCHEME, + // ) + Log.d(TAG, "当前是后台服务") + } // 启动QQ音乐登录 // CommonCmd.startQQMusicProcess(context,context.packageName) - CommonCmd.loginQQMusic( - context, - context.packageName, - BASE_SCHEME - ) - callback.onSuccess() + isBindQQMusicService = true + + Handler(Looper.getMainLooper()).postDelayed({ + callback.onSuccess() + }, 1000) // 延迟500ms + + // 等待用户登录完成后进行身份验证 // handler.postDelayed({ // verifyIdentity(callback) // }, 3000) // 给用户足够的登录时间 } - + private fun isAppInForeground(context: Context): Boolean { + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName = context.packageName + for (appProcess in appProcesses) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND && appProcess.processName == packageName) { + return true + } + } + return false + } /** * 启动授权流程 */ diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt index 5c87e6c94..c69c781cd 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt @@ -573,7 +573,8 @@ class QQMusicSingleton private constructor() { musicManager?.bindQQMusicApiService() callback(Result.failure(Exception("播放失败: $errorMessage"))) } else if (errorCode == 1030) { - callback(Result.success(Unit)) + callback(Result.failure(Exception("播放失败: $errorMessage"))) +// callback(Result.success(Unit)) }else{ callback(Result.failure(Exception("播放失败: $errorMessage"))) } From b2c5b69d4cbad6ae207f05f20af12500c7c1b300 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Fri, 19 Sep 2025 20:52:36 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E4=B8=8A=E4=BC=A0app=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/build.gradle.kts | 3 +- android/app/src/main/AndroidManifest.xml | 4 +- .../yunqiinnovation/deepsound/MainActivity.kt | 135 +++++++++++++++- .../deepsound/MyApplication.kt | 144 ++++++++++++++++++ lib/main.dart | 140 +++++++++++------ .../splash/controllers/splash_controller.dart | 15 ++ .../agent_service/AgentService.kt | 6 +- .../agent_service/AgentServiceImpl.swift | 11 +- .../qq_music/QQMusicLoginManager.kt | 12 +- 9 files changed, 408 insertions(+), 62 deletions(-) create mode 100644 android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 0b53e4a8f..6f29c8c95 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -61,7 +61,8 @@ android { manifestPlaceholders.putAll( mapOf( "redirectSchemeName" to "spotify-sdk", - "redirectHostName" to "auth" + "redirectHostName" to "auth", + "applicationName" to "com.saitong.voitrans.MyApp" // 新增:声明自定义 Application ) ) // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e1ca0f551..24d99722b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ xmlns:tools="http://schemas.android.com/tools"> + @@ -84,8 +85,7 @@ + Log.d(TAG, "🔗 处理待处理的深链接") + handleDeepLink(intent) + pendingDeepLink = null + } + }, 1000) // 延迟1秒确保Flutter完全准备好 + + Log.d(TAG, "✅ configureFlutterEngine 完成") + } + + // 处理深链接的统一方法 + private fun handleDeepLink(intent: Intent?) { + intent?.let { + if (it.action == Intent.ACTION_VIEW && it.data != null) { + val uri = it.data + Log.d(TAG, "🔗 处理深链接: $uri") + + try { + Log.d(TAG, "🔗 开始发送深链接到 Flutter") + + // 通过 MethodChannel 将深链接信息传递给 Flutter + flutterEngine?.dartExecutor?.binaryMessenger?.let { messenger -> + Log.d(TAG, "🔗 获取到 BinaryMessenger") + + val channel = MethodChannel( + messenger, + "com.saitong.voitrans/deeplink" + ) + + val arguments = mapOf( + "url" to uri.toString(), + "scheme" to uri?.scheme, + "host" to uri?.host, + "path" to uri?.path, + "query" to uri?.query, + "queryParameters" to uri?.queryParameterNames?.associateWith { + uri.getQueryParameter(it) + } + ) + + Log.d(TAG, "🔗 准备发送参数: $arguments") + + channel.invokeMethod("handleDeepLink", arguments) + Log.d(TAG, "✅ 深链接信息已发送到 Flutter") + } ?: run { + Log.w(TAG, "❌ BinaryMessenger 为空,无法发送深链接") + } + } catch (e: Exception) { + Log.e(TAG, "❌ 处理深链接时出错", e) + } + } else { + Log.d(TAG, "🔗 Intent 不是深链接或数据为空") + } + } } override fun onDestroy() { @@ -112,5 +237,5 @@ class MainActivity: FlutterActivity() { // 反射方法可能不存在,忽略 } } -} +} diff --git a/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt new file mode 100644 index 000000000..17e5ee3b0 --- /dev/null +++ b/android/app/src/main/kotlin/com/yunqiinnovation/deepsound/MyApplication.kt @@ -0,0 +1,144 @@ +package com.saitong.voitrans + +import android.app.ActivityManager +import android.content.Context +import android.util.Log +import io.flutter.app.FlutterApplication +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.FlutterEngineCache +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.plugins.GeneratedPluginRegistrant + +// Application.kt +class MyApp : FlutterApplication() { + + companion object { + private const val TAG = "MyApp" + private const val ENGINE_ID = "main_engine" + + // 标记 Engine 是否已经执行过 Dart 入口点 + @Volatile + private var isDartEntrypointExecuted = false + + // 记录应用启动时间,用于判断是否为新启动 + @Volatile + private var appStartTime = 0L + } + + override fun onCreate() { + super.onCreate() + appStartTime = System.currentTimeMillis() + Log.d(TAG, "MyApp onCreate - 进程启动") + + val cache = FlutterEngineCache.getInstance() + val existingEngine = cache.get(ENGINE_ID) + + if (existingEngine == null) { + Log.d(TAG, "创建新的 FlutterEngine") + createAndCacheEngine(cache) + } else { + Log.d(TAG, "FlutterEngine 已存在,检查状态") + // 检查现有 Engine 的状态 + try { + val isExecuting = existingEngine.dartExecutor.isExecutingDart + Log.d(TAG, "Engine Dart执行状态: $isExecuting") + + if (!isExecuting && isMainProcess()) { + Log.d(TAG, "Engine 存在但Dart未执行,重新执行") + existingEngine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + isDartEntrypointExecuted = true + } + } catch (e: Exception) { + Log.e(TAG, "检查或重新执行 Dart 入口点失败", e) + // 如果失败,重新创建 Engine + cache.remove(ENGINE_ID) + createAndCacheEngine(cache) + } + } + } + + private fun createAndCacheEngine(cache: FlutterEngineCache) { + // 在主进程中总是创建并执行Dart入口点 + if (isMainProcess()) { + Log.d(TAG, "✅ 在主进程创建 FlutterEngine 并执行 Dart") + val flutterEngine = FlutterEngine(this) + + // 注册插件(只在这里注册一次) + Log.d(TAG, "📦 注册 Flutter 插件") + GeneratedPluginRegistrant.registerWith(flutterEngine) + + try { + Log.d(TAG, "🚀 开始执行 Dart 入口点") + flutterEngine.dartExecutor.executeDartEntrypoint( + DartExecutor.DartEntrypoint.createDefault() + ) + isDartEntrypointExecuted = true + cache.put(ENGINE_ID, flutterEngine) + Log.d(TAG, "✅ FlutterEngine 创建并缓存成功") + } catch (e: Exception) { + Log.e(TAG, "❌ 执行 Dart 入口点失败", e) + // 如果执行失败,仍然缓存 Engine,但标记为未执行 + isDartEntrypointExecuted = false + cache.put(ENGINE_ID, flutterEngine) + } + } else { + Log.d(TAG, "⚠️ 在非主进程,仅创建 FlutterEngine") + // 非主进程只创建 Engine,不执行 Dart 入口点 + val flutterEngine = FlutterEngine(this) + cache.put(ENGINE_ID, flutterEngine) + } + } + + private fun isMainProcess(): Boolean { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processes = am.runningAppProcesses ?: return true + val mainProcessName = packageName + val myPid = android.os.Process.myPid() + + for (process in processes) { + if (process.pid == myPid) { + return process.processName == mainProcessName + } + } + return true + } + + private fun isAppInForeground(): Boolean { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val processes = am.runningAppProcesses ?: return false + val pkg = packageName + for (p in processes) { + if (p.processName == pkg && + p.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { + return true + } + } + return false + } + + private fun isAppBeingLaunched(): Boolean { + // 改进的启动检测逻辑 + val currentTime = System.currentTimeMillis() + val isRecentStart = (currentTime - appStartTime) < 3000 // 3秒内认为是新启动 + + if (isRecentStart) { + Log.d(TAG, "检测到应用新启动") + return true + } + + // 备用检测方法(兼容旧版本) + return try { + val am = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val tasks = am.getRunningTasks(1) + val result = tasks.isNotEmpty() && tasks[0].topActivity?.packageName == packageName + Log.d(TAG, "通过 RunningTasks 检测启动状态: $result") + result + } catch (e: Exception) { + Log.w(TAG, "无法获取 RunningTasks,可能缺少权限", e) + // 如果无法获取任务信息,默认认为是启动状态 + true + } + } +} diff --git a/lib/main.dart b/lib/main.dart index 6cfa3d668..5a0941bdf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,37 +1,104 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; -import 'package:flutter_intro/flutter_intro.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:get/get.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:get_storage/get_storage.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'core/bindings/initial_binding.dart'; + import 'routes/app_pages.dart'; import 'routes/app_routes.dart'; import 'core/translations/app_translations.dart'; import 'core/themes/app_theme.dart'; import 'core/themes/theme_service.dart'; -import '../../data/services/login_service.dart'; +import 'core/bindings/initial_binding.dart'; +import 'data/services/login_service.dart'; +import 'dart:ui' as ui; +import 'modules/splash/controllers/splash_controller.dart'; void main() async { - // 确保Flutter绑定初始化 WidgetsFlutterBinding.ensureInitialized(); - await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - - // 初始化存储 - 在创建应用之前初始化 + // 初始化存储 await GetStorage.init(); - // 初始化环境变量 - await dotenv.load(); + // 加载环境变量 + await dotenv.load(fileName: ".env"); + print("环境变量加载成功"); + + // 初始化登录服务 + Get.put(LoginService()); + + // 延迟初始化深链接处理器,确保Flutter完全准备好 + WidgetsBinding.instance.addPostFrameCallback((_) { + Future.delayed(const Duration(milliseconds: 1500), () { + _initDeepLinkHandler(); + }); + }); + + runApp(MainApp()); +} + +// 初始化深链接处理器 +void _initDeepLinkHandler() { + print("🔗 初始化深链接处理器"); + + const platform = MethodChannel('com.saitong.voitrans/deeplink'); + + platform.setMethodCallHandler((call) async { + print("🔗 Flutter 收到深链接调用: ${call.method}"); + + if (call.method == 'handleDeepLink') { + final arguments = call.arguments as Map; + print("🔗 深链接参数: $arguments"); - // 初始化登录服务(包含Firebase初始化) - LoginService(); + final url = arguments['url'] as String?; + if (url != null) { + print("🔗 处理深链接 URL: $url"); + await _handleDeepLinkNavigation(url); + } + } + }); - // 运行应用 - runApp(const MainApp()); + print("✅ 深链接处理器初始化完成"); +} + +// 处理深链接导航 +Future _handleDeepLinkNavigation(String url) async { + print("🔗 开始处理深链接导航: $url"); + + try { + final uri = Uri.parse(url); + print("🔗 解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}"); + + if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') { + print("🔗 检测到QQ音乐登录回调"); + + // 等待SplashController注册 + int retryCount = 0; + const maxRetries = 10; + + while (retryCount < maxRetries) { + try { + if (Get.isRegistered()) { + print("🔗 找到SplashController,处理QQ音乐登录"); + final controller = Get.find(); + controller.handleQQMusicLogin(); + return; + } + } catch (e) { + print("🔗 获取SplashController失败: $e"); + } + + retryCount++; + print("🔗 等待SplashController注册 ($retryCount/$maxRetries)"); + await Future.delayed(const Duration(milliseconds: 500)); + } + + print("❌ 超时等待SplashController注册"); + } + } catch (e) { + print("❌ 处理深链接时出错: $e"); + } } class MainApp extends StatelessWidget { @@ -39,46 +106,27 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - // 获取保存的主题设置 - final themeMode = ThemeService().getThemeMode(); + // 在应用构建时立即初始化深链接处理 + _initDeepLinkHandler(); + // 使用 ScreenUtilInit 包装整个应用 return ScreenUtilInit( - designSize: const Size(375, 812), + designSize: const Size(375, 812), // 设计稿尺寸,根据你的设计稿调整 minTextAdapt: true, splitScreenMode: true, - builder: (_, child) { + builder: (context, child) { return GetMaterialApp( - title: 'Voitrans', + title: 'VoiTrans', theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, - themeMode: themeMode, // 使用提前获取的主题模式 - initialBinding: InitialBinding(), + themeMode: ThemeService().getThemeMode(), initialRoute: Routes.splash, getPages: AppPages.pages, - debugShowCheckedModeBanner: false, + initialBinding: InitialBinding(), translations: AppTranslations(), - localizationsDelegates: const [ - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - locale: AppTranslations.deviceLocale(), - fallbackLocale: AppTranslations.getSmartFallbackLocale(), - supportedLocales: AppTranslations.supportedLocales, - builder: EasyLoading.init( - builder: (BuildContext context, Widget? widget) { - EasyLoading.instance.userInteractions = false; - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: TextScaler.noScaling, - ), - child: Intro( - padding: EdgeInsets.zero, - child: widget!, - ), - ); - }, - ), + locale: Get.deviceLocale, + fallbackLocale: const Locale('en', 'US'), + debugShowCheckedModeBanner: false, ); }, ); diff --git a/lib/modules/splash/controllers/splash_controller.dart b/lib/modules/splash/controllers/splash_controller.dart index 4da069bc9..1ac931498 100644 --- a/lib/modules/splash/controllers/splash_controller.dart +++ b/lib/modules/splash/controllers/splash_controller.dart @@ -289,6 +289,21 @@ class SplashController extends GetxController { return false; } } + + /// 处理QQ音乐登录回调 + void handleQQMusicLogin() { + try { + Logger.i(_tag, '处理QQ音乐登录回调'); + + // 这里可以添加具体的QQ音乐登录处理逻辑 + // 例如:验证登录状态、更新用户信息、导航到相应页面等 + + // 如果需要导航到特定页面,可以使用: + // Get.toNamed(Routes.qqmusic); + } catch (e) { + Logger.e(_tag, '处理QQ音乐登录回调失败: $e'); + } + } } /// 显示无网络连接对话框并等待用户操作(阻塞版本) 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 99e0ed8e5..c4cab4819 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 @@ -1956,7 +1956,7 @@ object AgentService : CoroutineScope { ) } if (playlist.isNotEmpty()) { - processQQMusicPlayList(playlist) +// processQQMusicPlayList(playlist) } else { Log.w(TAG, "qqmusic_searchandplay_songs 中没有有效的音乐条目") } @@ -1969,6 +1969,10 @@ object AgentService : CoroutineScope { val start = cardNavigation.optString("start", "") val end = cardNavigation.optString("end", "") processNavigation(start, end); + }else if (toolname == "map_weather") { //导航服务 + iscard = true + }else if (toolname == "maps_weather") { //导航服务 + iscard = true } return mapOf("meta" to metaStr, "broadcast" to broadcast, "notreply" to notreply,"iscard" to iscard); } diff --git a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index b294b6a05..b13d17233 100644 --- a/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -834,6 +834,7 @@ internal func autoHandleFunctionCallResult(toolName: String, functionCallResult: private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) -> [String: Any] { var broadcast = true var notreply = false + var iscard = false var metadata: [String: Any] = [:] metadata[toolName] = jsonDict @@ -843,7 +844,11 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - if let _notreply = jsonDict["notreply"] as? Bool { notreply = _notreply } + if let _iscard = jsonDict["iscard"] as? Bool { + iscard = _iscard + } metadata["notreply"] = notreply + metadata["iscard"] = iscard // 处理卡片数据 // if let cardMusic = jsonDict["card_music"] as? [String: Any] { // processMusicCard(cardMusic) @@ -854,8 +859,12 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - processNavigationCard(cardNavigation) } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { processQQMusicPlayList(cardQQMusiclist) + } else if toolName == "maps_weather" { + iscard = true + }else if toolName == "map_weather" { + iscard = true } - + metadata["iscard"] = iscard let metaStr: String if let jsonData = try? JSONSerialization.data(withJSONObject: metadata, options: []), let jsonString = String(data: jsonData, encoding: .utf8) { diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt index 396c6cf76..077fdf10c 100644 --- a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt @@ -108,7 +108,7 @@ class QQMusicLoginManager(private val context: Context) { // 已授权,直接请求登录信息 Log.d(TAG, "已授权,直接获取登录信息") // isBindQQMusicService = true - startAuthProcess2(callback) + startAuthProcess(callback) } ErrorCodes.ERROR_API_NO_PERMISSION -> { // 需要授权 @@ -156,11 +156,11 @@ class QQMusicLoginManager(private val context: Context) { ) // Log.d(TAG, "当前是前台服务") } else { - // CommonCmd.loginQQMusic( - // context, - // context.packageName, - // BASE_SCHEME, - // ) + CommonCmd.loginQQMusic( + context, + context.packageName, + BASE_SCHEME, + ) Log.d(TAG, "当前是后台服务") } // 启动QQ音乐登录