diff --git a/apps/client/android/app/build.gradle.kts b/apps/client/android/app/build.gradle.kts index 49ba147f..51e7533c 100644 --- a/apps/client/android/app/build.gradle.kts +++ b/apps/client/android/app/build.gradle.kts @@ -3,9 +3,6 @@ import java.io.FileInputStream repositories { google() mavenCentral() - flatDir { - dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行 - } } plugins { diff --git a/apps/client/android/build.gradle.kts b/apps/client/android/build.gradle.kts index 6359ffd1..ef2d5d74 100644 --- a/apps/client/android/build.gradle.kts +++ b/apps/client/android/build.gradle.kts @@ -5,11 +5,6 @@ allprojects { mavenCentral() maven { url = uri("https://storage.googleapis.com/download.flutter.io") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } - - // 修改为绝对路径,避免相对路径计算错误 - flatDir { - dirs(file("${rootProject.projectDir}/../local_plugins/qq_music/android/libs")) - } } } diff --git a/apps/client/android/settings.gradle.kts b/apps/client/android/settings.gradle.kts index 2571d00c..b9350375 100644 --- a/apps/client/android/settings.gradle.kts +++ b/apps/client/android/settings.gradle.kts @@ -39,7 +39,6 @@ include(":classic_bluetooth") include(":agent_service") include(":speech") include(":chat_api") -include(":qq_music") // 添加这行 include(":bluetooth_manager") // 恒玄(BES)经典蓝牙连接 + G722/mSBC 音频编解码 // 设置项目路径 @@ -50,5 +49,4 @@ project(":classic_bluetooth").projectDir = file("../local_plugins/classic_blueto project(":agent_service").projectDir = file("../local_plugins/agent_service/android") project(":speech").projectDir = file("../local_plugins/speech/android") project(":chat_api").projectDir = file("../local_plugins/chat_api/android") -project(":qq_music").projectDir = file("../local_plugins/qq_music/android") // 添加这行 project(":bluetooth_manager").projectDir = file("../local_plugins/bluetooth_manager/android") diff --git a/apps/client/ios/Runner/AppDelegate.swift b/apps/client/ios/Runner/AppDelegate.swift index 16268171..2348427b 100644 --- a/apps/client/ios/Runner/AppDelegate.swift +++ b/apps/client/ios/Runner/AppDelegate.swift @@ -30,20 +30,6 @@ import UIKit return true } - // 检查是否是Eaimar相关的URL - if url.scheme == "eaimar" { - print("处理 eaimar URL: \(url.absoluteString)") - - // 通过Flutter的方法通道通知QQ音乐插件处理这个URL - if let flutterViewController = window?.rootViewController as? FlutterViewController { - let channel = FlutterMethodChannel(name: "qq_music", binaryMessenger: flutterViewController.binaryMessenger) - channel.invokeMethod("openUrl", arguments: ["url": url.absoluteString]) { result in - print("QQ音乐插件处理URL结果: \(String(describing: result))") - } - } - return true - } - return false } } diff --git a/apps/client/ios/Runner/Info.plist b/apps/client/ios/Runner/Info.plist index 980c66a8..b9a5caa3 100644 --- a/apps/client/ios/Runner/Info.plist +++ b/apps/client/ios/Runner/Info.plist @@ -65,7 +65,6 @@ App-Prefs prefs - qqmusic weixin weixinULAPI weixinURLParamsAPI diff --git a/apps/client/lib/core/bindings/initial_binding.dart b/apps/client/lib/core/bindings/initial_binding.dart index 05296a95..b924068d 100644 --- a/apps/client/lib/core/bindings/initial_binding.dart +++ b/apps/client/lib/core/bindings/initial_binding.dart @@ -1,6 +1,5 @@ import '../../data/services/meeting/meeting_task_service.dart'; import '/data/services/pay_service.dart'; -import '/data/services/qq_music.dart'; import '../../../data/services/user_portrait.dart'; @@ -132,10 +131,7 @@ class InitialBinding extends Bindings { Get.lazyPut(() => UserportraitManager(), fenix: true); // qq音乐服务 - Get.lazyPut(() => QqMusicService(), fenix: true); - // 强制初始化 qq_music 插件 - // Get.find().initializePlugin(); // 新手引导 Get.lazyPut(() => NoviceGuideService(), fenix: true); diff --git a/apps/client/lib/data/services/qq_music.dart b/apps/client/lib/data/services/qq_music.dart deleted file mode 100644 index 0f37e16e..00000000 --- a/apps/client/lib/data/services/qq_music.dart +++ /dev/null @@ -1,424 +0,0 @@ -import 'dart:async'; -import 'package:get/get.dart'; -import 'package:qq_music/qq_music.dart'; -import '../models/song_model.dart'; - -/// QQ音乐服务适配器 - 全局状态管理 -class QqMusicService extends GetxService { - static QqMusicService get to => Get.find(); - - var isInitialized = false; - // 音乐服务实例 - final QqMusic _qqMusic = QqMusic(); - - // 全局音乐状态 - final isPlaying = false.obs; - final currentPosition = 0.obs; // 毫秒 - final totalDuration = 0.obs; // 毫秒 - final currentSong = Rxn(); - - // 状态监听定时器 - Timer? _stateMonitorTimer; - - @override - void onInit() { - super.onInit(); - // print('QQ音乐 全局音乐服务初始化'); - // 立即初始化插件 - // initializePlugin(); - } - - @override - void onClose() { - _stateMonitorTimer?.cancel(); - - print('🎵 [QQMusicService] 全局音乐服务销毁'); - super.onClose(); - } - - /// 启动播放状态监控 - void startPlaybackStateMonitor() { - // 取消之前的定时器 - _stateMonitorTimer?.cancel(); - - print('🎵 [QQMusicService] 启动全局状态监控'); - - // 立即执行一次同步 - _syncPlaybackInfo(); - - // 改为每2-3秒同步一次,减少频率 - _stateMonitorTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - _syncPlaybackInfo(); - }); - - print('🎵 [QQMusicService] 全局状态监控已启动'); - } - - /// 停止播放状态监控 - void stopPlaybackStateMonitor() { - _stateMonitorTimer?.cancel(); - _stateMonitorTimer = null; - print('⏹️ [QQMusicService] 全局状态监控已停止'); - } - - /// 统一同步播放信息 - bool _isSyncing = false; - - Future _syncPlaybackInfo() async { - if (_isSyncing) { - print('🔄 [QQMusicService] 上次同步未完成,跳过本次'); - return; - } - - _isSyncing = true; - try { - final result = await _qqMusic - .syncCurrentPlayInfo() - .timeout(const Duration(milliseconds: 500)); // 500ms超时 - - if (result['success'] == true && result['data'] != null) { - final data = result['data']; - - // 更新播放状态 - final newIsPlaying = data['isPlaying'] ?? false; - final wasPlaying = isPlaying.value; - - if (isPlaying.value != newIsPlaying) { - print( - '🎮 [QQMusicService] 全局播放状态变化: ${isPlaying.value} -> $newIsPlaying'); - isPlaying.value = newIsPlaying; - } - - // 如果从播放状态变为停止状态,自动停止监听 - if (wasPlaying && !newIsPlaying) { - print('⏹️ [QQMusicService] 检测到播放停止,自动停止全局监听'); - stopPlaybackStateMonitor(); - return; - } - - // 如果当前不在播放状态,也停止监听 - if (!newIsPlaying) { - print('⏸️ [QQMusicService] 当前未在播放,停止全局监听以节省资源'); - stopPlaybackStateMonitor(); - return; - } - - // 更新播放时间(Android端返回秒,转换为毫秒) - final currentTimeSec = data['currentTime'] ?? 0; - final totalTimeSec = data['totalTime'] ?? 0; - - final newCurrentPosition = (currentTimeSec * 1000).round(); - final newTotalDuration = (totalTimeSec * 1000).round(); - - if (currentPosition.value != newCurrentPosition) { - currentPosition.value = newCurrentPosition; - } - if (totalDuration.value != newTotalDuration) { - totalDuration.value = newTotalDuration; - } - - // 更新当前歌曲信息 - final songData = data['currentSong']; - if (songData != null) { - final newSong = SongModel( - mid: songData['mid'] ?? '', - name: songData['name'] ?? '', - artist: songData['singer'] ?? '', - album: songData['album'] ?? '', - albumPic: songData['coverUri'], - duration: songData['duration'] ?? 0, - ); - - if (currentSong.value?.mid != newSong.mid) { - print( - '🎵 [QQMusicService] 当前歌曲变化: ${currentSong.value?.name} -> ${newSong.name}'); - currentSong.value = newSong; - } - } - } else { - // 同步失败时停止监听 - print('❌ [QQMusicService] 全局同步失败,停止监听'); - stopPlaybackStateMonitor(); - } - } on TimeoutException { - print('⏰ [QQMusicService] 同步超时,跳过本次更新'); - } catch (e) { - print('💥 [QQMusicService] 全局同步异常: $e'); - stopPlaybackStateMonitor(); - } finally { - _isSyncing = false; - } - } - - /// 格式化时间 - String formatDuration(int milliseconds) { - final duration = Duration(milliseconds: milliseconds); - final minutes = duration.inMinutes; - final seconds = duration.inSeconds % 60; - return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - - /// 登录QQ音乐 - Future> login() async { - try { - final result = await _qqMusic.login(); - if (result['success'] == true) { - isInitialized = true; - return result; - } else { - return {'success': false, 'error': result['error'] ?? '登录失败'}; - } - } catch (e) { - return {'success': false, 'error': '登录异常: ${e.toString()}'}; - } - } - - /// 搜索歌曲 - Future> searchSongs({ - required String keyword, - int searchType = 0, - }) async { - try { - final result = await _qqMusic.searchSongs( - keyword: keyword, - searchType: searchType, - ); - return result; - } catch (e) { - return {'success': false, 'error': '搜索失败: ${e.toString()}'}; - } - } - - /// 播放歌曲列表 - 更新全局状态 - Future> playSongs({ - required List songMids, - int index = 0, - List? songList, - SongModel? currentSongModel, - }) async { - try { - stopPlaybackStateMonitor(); - isPlaying.value = true; - currentPosition.value = 0; - currentSong.value = currentSongModel; - final result = await _qqMusic.playSongs( - songMids: songMids, - index: index, - ); - print('💥 [QQMusicService] playSongs: $result'); - if (result['success'] == true) { - if (currentSongModel != null) { - currentSong.value = currentSongModel; - totalDuration.value = currentSongModel.duration; - } - isPlaying.value = true; - currentPosition.value = 0; - // 启动全局状态监听 - Future.delayed(Duration(seconds: 1), () { - syncCurrentPlayInfo(); - }); - } else { - isPlaying.value = false; - currentPosition.value = 0; - currentSong.value = null; - } - - return result; - } catch (e) { - return {'success': false, 'error': '播放失败: ${e.toString()}'}; - } - } - - /// 播放音乐 - Future> playMusic() async { - try { - final result = await _qqMusic.playMusic(); - if (result['success'] == true) { - isPlaying.value = true; - currentPosition.value = 0; - // 启动全局状态监听 - Future.delayed(Duration(milliseconds: 500), () { - syncCurrentPlayInfo(); - }); - } - return result; - } catch (e) { - return {'success': false, 'error': '播放失败: ${e.toString()}'}; - } - } - - /// 暂停音乐 - Future> pauseMusic() async { - try { - final result = await _qqMusic.pauseMusic(); - if (result['success'] == true) { - isPlaying.value = false; - } - return result; - } catch (e) { - return {'success': false, 'error': '暂停失败: ${e.toString()}'}; - } - } - - /// 自动暂停音乐 - Future autopauseMusic() async { - try { - final result = await _qqMusic.autopauseMusic(); - if (result['success'] == true) { - isPlaying.value = false; - } - return result['success'] == true; - } catch (e) { - return false; - } - } - - /// 继续播放 - Future> resumeMusic() async { - try { - final result = await _qqMusic.resumeMusic(); - if (result['success'] == true) { - isPlaying.value = true; - startPlaybackStateMonitor(); - } - return result; - } catch (e) { - return {'success': false, 'error': '继续播放失败: ${e.toString()}'}; - } - } - - /// 停止音乐 - Future> stopMusic() async { - try { - final result = await _qqMusic.stopMusic(); - if (result['success'] == true) { - isPlaying.value = false; - currentPosition.value = 0; - stopPlaybackStateMonitor(); - } - return result; - } catch (e) { - return {'success': false, 'error': '停止失败: ${e.toString()}'}; - } - } - - /// 下一首 - Future> skipToNext() async { - try { - final result = await _qqMusic.skipToNext(); - if (result['success'] == true) {} - return result; - } catch (e) { - return {'success': false, 'error': '下一首失败: ${e.toString()}'}; - } - } - - /// 上一首 - Future> skipToPrevious() async { - try { - final result = await _qqMusic.skipToPrevious(); - if (result['success'] == true) {} - return result; - } catch (e) { - return {'success': false, 'error': '上一首失败: ${e.toString()}'}; - } - } - - /// 获取播放状态 - Future> getPlaybackState() async { - try { - final result = await _qqMusic.getPlaybackState(); - return result; - } catch (e) { - return {'success': false, 'error': '获取播放状态失败: ${e.toString()}'}; - } - } - - /// 获取当前播放歌曲 - Future> getCurrentSong() async { - try { - final result = await _qqMusic.getCurrentSong(); - return result; - } catch (e) { - return {'success': false, 'error': '获取当前歌曲失败: ${e.toString()}'}; - } - } - - /// 获取播放时间 - Future> getPlayTime() async { - try { - final result = await _qqMusic.getPlayTime(); - return result; - } catch (e) { - return {'success': false, 'error': '获取播放时间失败: ${e.toString()}'}; - } - } - - /// 同步当前播放信息 - 统一接口 - Future> syncCurrentPlayInfo( - {bool isOpenPlay = false}) async { - try { - _stateMonitorTimer?.cancel(); //先关闭计时器 - final result = await _qqMusic.syncCurrentPlayInfo(); - print('💥 [QQMusicService] syncCurrentPlayInfo: $result'); - // 如果同步成功,更新全局状态 - if (result['success'] == true && result['data'] != null) { - final data = result['data']; - - // 更新播放状态 - final newIsPlaying = data['isPlaying'] ?? false; - if (isPlaying.value != newIsPlaying) { - isPlaying.value = newIsPlaying; - } - - // 更新播放时间(Android端返回秒,转换为毫秒) - final currentTimeSec = data['currentTime'] ?? 0; - final totalTimeSec = data['totalTime'] ?? 0; - - final newCurrentPosition = (currentTimeSec * 1000).round(); - final newTotalDuration = (totalTimeSec * 1000).round(); - - if (currentPosition.value != newCurrentPosition) { - currentPosition.value = newCurrentPosition; - } - if (totalDuration.value != newTotalDuration) { - totalDuration.value = newTotalDuration; - } - - // 更新当前歌曲信息 - final songData = data['currentSong']; - if (songData != null) { - final newSong = SongModel( - mid: songData['mid'] ?? '', - name: songData['name'] ?? '', - artist: songData['singer'] ?? '', - album: songData['album'] ?? '', - albumPic: songData['coverUri'], - duration: songData['duration'] ?? 0, - ); - - if (currentSong.value?.mid != newSong.mid) { - currentSong.value = newSong; - } - } - if (newIsPlaying) { - //正在播放 - startPlaybackStateMonitor(); - } else { - stopPlaybackStateMonitor(); - currentSong.value = null; - - if (isOpenPlay) { - Future.delayed(Duration(seconds: 2), () { - syncCurrentPlayInfo(); - }); - } - } - } - - return result; - } catch (e) { - return {'success': false, 'error': '同步播放信息失败: ${e.toString()}'}; - } - } -} diff --git a/apps/client/lib/main.dart b/apps/client/lib/main.dart index a05db60d..27b14f4b 100644 --- a/apps/client/lib/main.dart +++ b/apps/client/lib/main.dart @@ -24,7 +24,6 @@ import 'core/services/native_localization_sync.dart'; import 'core/services/ai_orb_controller.dart'; import 'data/services/login_service.dart'; import 'modules/meeting/controllers/meeting_home_controller.dart'; -import 'modules/splash/controllers/splash_controller.dart'; bool _deepLinkHandlerInitialized = false; @@ -109,32 +108,7 @@ Future _handleDeepLinkNavigation(String url) async { Logger.d("DeepLink", "解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}"); - if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') { - Logger.i("DeepLink", "检测到QQ音乐登录回调"); - - // 等待SplashController注册 - int retryCount = 0; - const maxRetries = 10; - - while (retryCount < maxRetries) { - try { - if (Get.isRegistered()) { - Logger.i("DeepLink", "找到SplashController,处理QQ音乐登录"); - final controller = Get.find(); - controller.handleQQMusicLogin(); - return; - } - } catch (e) { - Logger.e("DeepLink", "获取SplashController失败", e); - } - - retryCount++; - Logger.d("DeepLink", "等待SplashController注册 ($retryCount/$maxRetries)"); - await Future.delayed(const Duration(milliseconds: 500)); - } - - Logger.w("DeepLink", "超时等待SplashController注册"); - } + // 目前没有需要处理的深链接(原 deepvoice://qqmusic 的 QQ 音乐登录回调已随 QQ 音乐一起下线)。 } catch (e) { Logger.e("DeepLink", "处理深链接时出错", e); } diff --git a/apps/client/lib/modules/splash/controllers/splash_controller.dart b/apps/client/lib/modules/splash/controllers/splash_controller.dart index 8f86412b..e882b1da 100644 --- a/apps/client/lib/modules/splash/controllers/splash_controller.dart +++ b/apps/client/lib/modules/splash/controllers/splash_controller.dart @@ -326,21 +326,6 @@ class SplashController extends GetxController { Logger.e(_tag, 'initMeetingTemplate 获取失败: $e'); } } - - /// 处理QQ音乐登录回调 - void handleQQMusicLogin() { - try { - Logger.i(_tag, '处理QQ音乐登录回调'); - - // 这里可以添加具体的QQ音乐登录处理逻辑 - // 例如:验证登录状态、更新用户信息、导航到相应页面等 - - // 如果需要导航到特定页面,可以使用: - // Get.toNamed(Routes.qqmusic); - } catch (e) { - Logger.e(_tag, '处理QQ音乐登录回调失败: $e'); - } - } } /// 显示无网络连接对话框并等待用户操作(阻塞版本) diff --git a/apps/client/lib/modules/translation/controllers/translation_controller.dart b/apps/client/lib/modules/translation/controllers/translation_controller.dart index 473fd363..467d3c95 100644 --- a/apps/client/lib/modules/translation/controllers/translation_controller.dart +++ b/apps/client/lib/modules/translation/controllers/translation_controller.dart @@ -15,7 +15,6 @@ import 'package:floating_ui_plugin/native_plugin.dart'; import 'package:path_provider/path_provider.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; -import 'package:eaimar/data/services/qq_music.dart'; import '../../../data/services/ast_service.dart'; import '../../../data/services/server_translation_service.dart'; import '../../../data/services/music_manager.dart'; @@ -122,7 +121,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { static const int _astReconnectMaxDelaySec = 8; final GetStorage _storage = GetStorage(); final _musiceManager = Get.find(); - final _qqMusicManager = Get.find(); late final UsageStatsService _usageService; // 安卓悬浮窗管理器 late final FloatingWindowManager _floatingWindowManager; @@ -141,7 +139,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { bool _audioSourceType = false; // 音频输入源类型 bool isPlayback = false; // 是否开启播放 bool isContinuePlaying = false; // 是否继续播放 - bool _isContinueQqMusicPlaying = false; // 是否继续播放QQ音乐 // 存储相关配置 static const String _sourceLanguageKey = 'translation_source_language'; @@ -993,11 +990,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver { } } - if (!_isContinueQqMusicPlaying) { - if (await _qqMusicManager.autopauseMusic()) { - _isContinueQqMusicPlaying = true; - } - } } catch (e) { Logger.error('暂停其他服务失败: ${e.toString()}'); } @@ -1010,12 +1002,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver { Future restoreOtherServices() async { try { if (isContinuePlaying) await _musiceManager.resume(); - if (_isContinueQqMusicPlaying) await _qqMusicManager.resumeMusic(); } catch (e) { Logger.error('恢复其他服务失败: ${e.toString()}'); } finally { isContinuePlaying = false; - _isContinueQqMusicPlaying = false; } } diff --git a/apps/client/local_plugins/agent_service/android/build.gradle.kts b/apps/client/local_plugins/agent_service/android/build.gradle.kts index fc1a06f6..88d98ae9 100644 --- a/apps/client/local_plugins/agent_service/android/build.gradle.kts +++ b/apps/client/local_plugins/agent_service/android/build.gradle.kts @@ -52,6 +52,5 @@ dependencies { add("compileOnly", project(":chat_api")) add("compileOnly", project(":location_service")) add("compileOnly", project(":user_portrait")) - add("compileOnly", project(":qq_music")) } \ No newline at end of file diff --git a/apps/client/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt b/apps/client/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt index 1dd9cb03..b613359d 100644 --- a/apps/client/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt +++ b/apps/client/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt @@ -17,7 +17,6 @@ import com.yunqiinnovation.music_service.MusicService import com.yunqiinnovation.amap_service.NavigationServiceHelper import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.location_service.LocationService -import com.yunqiinnovation.qq_music.QQMusicSingleton import android.util.Log import kotlinx.coroutines.* import java.util.Collections @@ -544,7 +543,6 @@ object AgentService : CoroutineScope { //初始化定位服务 LocationService.initialize(appContext) MusicSingleton.getInstance().initialize(appContext,serverurl,usertoken) - QQMusicSingleton.getInstance().initialize(appContext) // 设置系统提示词 val customSystemPrompt = config["systemPrompt"] as? String //保存配置 @@ -1660,7 +1658,7 @@ object AgentService : CoroutineScope { if (name == "exit_interaction") { BleService.closeCodec() stopRecognition() - }else if (name == "qqmusic_pause" || name == "qqmusic_stop" || name == "music_pause" || name == "music_close"){ + }else if (name == "music_pause" || name == "music_close"){ isplaymusic = false } _log("INFO", "[Agent] AI onFunctionCall: callingTool=$iscallingTool") @@ -2572,40 +2570,7 @@ object AgentService : CoroutineScope { } else { Log.w(TAG, "card_musiclist.musics 不是一个数组") } - } else if (toolname == "qqmusic_search_songs" && meta.has("data")) { //qq音乐 - //"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}" - val musics = meta.optJSONArray("data") - if (musics != null) { - val playlist = mutableListOf>() - for (i in 0 until musics.length()) { - val item = musics.optJSONObject(i) ?: continue - val mid = item.optString("mid", "") - val name = item.optString("name", "") - val artist = item.optString("artist", "") - val album = item.optString("album", "") - val duration = item.optInt("duration", 0) - val pic = item.optLong("pic", 0) - playlist.add( - mapOf( - "mid" to mid, - "name" to name, - "artist" to artist, - "album" to album, - "duration" to duration, - "pic" to pic - ) - ) - } - if (playlist.isNotEmpty()) { - processQQMusicPlayList(playlist) - } else { - Log.w(TAG, "qqmusic_searchandplay_songs 中没有有效的音乐条目") - } - } else { - Log.w(TAG, "qqmusic_searchandplay_songs.musics 不是一个数组") - } - - }else if (meta.has("card_navigation")) { //导航服务 + } else if (meta.has("card_navigation")) { //导航服务 val cardNavigation = meta.getJSONObject("card_navigation") val start = cardNavigation.optString("start", "") val destination = cardNavigation.optString("destination", "") @@ -2672,39 +2637,6 @@ object AgentService : CoroutineScope { Log.e(TAG, "音乐列表为空,未启动播放服务") } } - /** - * 处理音乐播放列表 - * 将ai返回的音乐连接 在后台启动播放 - * - * @param musicUrl 图片文件路径 - * @param name 歌曲名称,默认为空 - * @param singer 歌手,默认为false - * @return 是否成功开始处理 - */ - fun processQQMusicPlayList(songs: List>) { - // 如果 playlist 中有有效的歌曲,开始播放 - if (songs.isNotEmpty()) { - val qqMusicSingleton = QQMusicSingleton.getInstance() - // 从songs中提取mid字段构建songMids列表 - val songMids = songs.mapNotNull { song -> - song["mid"] as? String - }.filter { it.isNotEmpty() } - Log.w(TAG, "processQQMusicPlayList 开始播放音乐 $songMids") - if (songMids.isNotEmpty()) { - qqMusicSingleton.playSongs(songMids, 0) { result -> - result.onSuccess { - Log.i(TAG, "QQ音乐播放列表启动成功,共${songMids.size}首歌曲") - }.onFailure { exception -> - Log.e(TAG, "QQ音乐播放列表启动失败: ${exception.message}") - } - } - } else { - Log.e(TAG, "音乐列表中没有有效的mid,未启动播放服务") - } - } else { - Log.e(TAG, "音乐列表为空,未启动播放服务") - } - } /** * 处理导航 * 启动导航服务到指定目的地 @@ -3240,16 +3172,7 @@ object AgentService : CoroutineScope { */ private fun autopauseMusic(){ _log("INFO", "[Agent][音乐] autopauseMusic 进入 type=$musicServiceType isplaymusic=$isplaymusic") - if ( musicServiceType == "QQ" ){ - QQMusicSingleton.getInstance().autopauseMusic { result -> - if (result.isSuccess) { - isplaymusic = true - _log("INFO", "[Agent][音乐] QQ 音乐暂停成功,isplaymusic=true") - } else { - _log("WARNING", "[Agent][音乐] QQ 音乐暂停失败: ${result.exceptionOrNull()?.message}") - } - } - }else if ( musicServiceType == "WY" ){ + if ( musicServiceType == "WY" ){ if (MusicService.isPlaying()){ MusicSingleton.startServiceWithCommand(context, "pause") isplaymusic = true @@ -3258,7 +3181,7 @@ object AgentService : CoroutineScope { _log("INFO", "[Agent][音乐] WY 当前未在播放,跳过暂停") } } else { - _log("INFO", "[Agent][音乐] 未配置 QQ/WY 内置音乐源,跳过 autopauseMusic") + _log("INFO", "[Agent][音乐] 未配置 WY 内置音乐源,跳过 autopauseMusic") } } @@ -3266,10 +3189,7 @@ object AgentService : CoroutineScope { _log("INFO", "[Agent][音乐] resumeMusic 进入 type=$musicServiceType isplaymusic=$isplaymusic") if ( isplaymusic ){ isplaymusic = false - if ( musicServiceType == "QQ" ){ - _log("INFO", "[Agent][音乐] 调用 QQMusic.resumeMusic") - QQMusicSingleton.getInstance().resumeMusic {} - }else if ( musicServiceType == "WY" ){ + if ( musicServiceType == "WY" ){ _log("INFO", "[Agent][音乐] 调用 WY MusicSingleton resume") MusicSingleton.startServiceWithCommand(context, "resume") } else { diff --git a/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift b/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift index e2038122..1ea83a69 100644 --- a/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift +++ b/apps/client/local_plugins/agent_service/ios/agent_service/Package.swift @@ -16,7 +16,6 @@ let package = Package( .package(path: "../../../location_service/ios/location_service"), .package(path: "../../../music_service/ios/music_service"), .package(path: "../../../user_portrait/ios/user_portrait"), - .package(path: "../../../qq_music/ios/qq_music"), ], targets: [ .target( @@ -30,7 +29,6 @@ let package = Package( .product(name: "location-service", package: "location_service"), .product(name: "music-service", package: "music_service"), .product(name: "user-portrait", package: "user_portrait"), - .product(name: "qq-music", package: "qq_music"), ], path: "Sources/agent_service", resources: [ diff --git a/apps/client/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift b/apps/client/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift index 0db3caec..71b478f2 100644 --- a/apps/client/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift +++ b/apps/client/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift @@ -11,7 +11,6 @@ import chat_api import chat_storage import music_service import user_portrait -import qq_music protocol AgentServiceListener: AnyObject { func onEvent(eventName: String, data: [String: Any]) @@ -1178,8 +1177,6 @@ private func handleJSONDictionary(_ jsonDict: [String: Any], toolName: String) - } else if let cardNavigation = jsonDict["card_navigation"] as? [String: Any] { let isinternal = processNavigationCard(cardNavigation) metadata["isinternal"] = isinternal - } else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" { - processQQMusicPlayList(cardQQMusiclist) } else if toolName == "map_weather" { iscard = true }else if toolName == "maps_weather" { @@ -1232,31 +1229,6 @@ private func jsonToString(_ json: [String: Any]) -> String? { return nil } } - private func processQQMusicPlayList(_ musics: [[String: Any]]){ - // 如果 playlist 中有有效的歌曲,开始播放 - if !musics.isEmpty { - // 从songs中提取mid字段构建songMids列表 - let songMids = musics.compactMap { song in - song["mid"] as? String - }.filter { !$0.isEmpty } - - if !songMids.isEmpty { - // 调用QPlayAutoManager的playSongs方法 - - QPlayAutoManager.shared.playSongs(mids: songMids, index: 0) { success, errorMessage in - if success { - print("QQ音乐播放列表启动成功,共\(songMids.count)首歌曲") - } else { - print("QQ音乐播放列表启动失败: \(errorMessage ?? "未知错误")") - } - } - } else { - print("音乐列表中没有有效的mid,未启动播放服务") - } - } else { - print("音乐列表为空,未启动播放服务") - } - } // 处理音乐卡片 private func processMusicCard(_ cardMusic: [String: Any]) { let id = cardMusic["id"] as? Int ?? 0 @@ -2100,7 +2072,7 @@ class ChatApiStreamCallback: StreamCallback { // 关闭编码器 (设备侧) BleService.shared.closeCodec() agentService.stopRecognition() - }else if (name == "qqmusic_pause" || name == "qqmusic_stop" || name == "music_pause" || name == "music_close"){ // + }else if (name == "music_pause" || name == "music_close"){ // agentService.wasMusicPlayingBeforeRecognition = false } diff --git a/apps/client/local_plugins/chat_api/android/build.gradle.kts b/apps/client/local_plugins/chat_api/android/build.gradle.kts index 44bb954b..251143fb 100644 --- a/apps/client/local_plugins/chat_api/android/build.gradle.kts +++ b/apps/client/local_plugins/chat_api/android/build.gradle.kts @@ -61,5 +61,4 @@ dependencies { add("compileOnly", project(":location_service")) add("compileOnly", project(":music_service")) - add("compileOnly", project(":qq_music")) } \ No newline at end of file diff --git a/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt b/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt index b8013e6d..4de7148b 100644 --- a/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt +++ b/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt @@ -58,14 +58,9 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO") Log.d(TAG, "初始化音乐服务: $musicType") when (musicType) { "WY" -> initializeMusiceFunctions() //网易 - "QQ" -> initializeQQMusiceFunctions() //QQ - "AUTO" -> { - if (QQMusicSystemFunctionHandler.CheckQQMusicInstall()) { //检查是否安装qq音乐 - initializeQQMusiceFunctions(); - }else{ - initializeMusiceFunctions() - } - }else -> { + // QQ 音乐已于 2026-09-24 下线,"QQ"/"AUTO" 一律走网易 + "QQ", "AUTO" -> initializeMusiceFunctions() + else -> { Log.w(TAG, "未指定音乐服务类型,默认使用网易") } } @@ -94,17 +89,6 @@ class MCPClient(private val context: Context? = null,musicType: String = "AUTO") Log.w(TAG, "Failed to initialize system functions", e) } } - /** - * 初始化音乐函数 - */ - private fun initializeQQMusiceFunctions() { - try { - val handler = QQMusicSystemFunctionHandler(context) - handler.registerAllFunctions(this) - } catch (e: Exception) { - Log.w(TAG, "Failed to initialize system functions", e) - } - } /** * 连接到SSE服务器 diff --git a/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt b/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt deleted file mode 100644 index 37f405c6..00000000 --- a/apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt +++ /dev/null @@ -1,460 +0,0 @@ -package com.yunqiinnovation.chat_api - -import android.content.Context -import android.util.Log -import kotlinx.coroutines.delay -import com.yunqiinnovation.qq_music.QQMusicSingleton -import kotlin.coroutines.suspendCoroutine -import kotlin.coroutines.resume -import kotlinx.coroutines.CompletableDeferred -/** - * QQ音乐系统功能处理器 - * 负责注册QQ音乐相关的MCP函数 - */ -class QQMusicSystemFunctionHandler(private val context: Context? = null) { - companion object { - const val TAG = "QQMusicSystemFunctionHandler" - - fun CheckQQMusicInstall(): Boolean { - val qqMusicSingleton = QQMusicSingleton.getInstance() - return qqMusicSingleton.isQQMusicInstalled() - } - } - - - - /** - * 注册所有QQ音乐相关函数到MCPClient - */ - fun registerAllFunctions(client: MCPClient) { - try { - // 注册QQ音乐搜索功能(仅搜索,不播放) - client.registerLocalFunction( - "qqmusic_search_songs", - "在QQ音乐中搜索歌曲,支持歌曲、专辑、歌手等搜索类型", - mapOf( - "type" to "object", - "properties" to mapOf( - "keyword" to mapOf( - "type" to "string", - "description" to "搜索关键词,歌曲名、专辑名或歌手名或者类型" - ), - "searchType" to mapOf( - "type" to "integer", - "description" to "搜索类型:0-歌名搜索,1-歌手搜索,2-歌曲类型搜索", - "default" to 0 - ) - ), - "required" to listOf("keyword") - ), - QQMusicSearchOnlyHandler(context) - ) - - // 注册QQ音乐播放歌曲列表功能 - client.registerLocalFunction( - "qqmusic_play_songs", - "播放指定的QQ音乐歌曲列表", - mapOf( - "type" to "object", - "properties" to mapOf( - "songMids" to mapOf( - "type" to "array", - "items" to mapOf("type" to "string"), - "description" to "歌曲MID列表" - ), - "index" to mapOf( - "type" to "integer", - "description" to "开始播放的歌曲索引,默认为0", - "default" to 0 - ) - ), - "required" to listOf("songMids") - ), - QQMusicPlaySongsHandler(context) - ) - - client.registerLocalFunction( - "qqmusic_pause", - "暂停QQ音乐", - mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - QQMusicPauseHandler(context) - ) - - client.registerLocalFunction( - "qqmusic_resume", - "恢复播放QQ音乐", - mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - QQMusicResumeHandler(context) - ) - - client.registerLocalFunction( - "qqmusic_stop", - "停止QQ音乐播放", - mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - QQMusicStopHandler(context) - ) - - client.registerLocalFunction( - "qqmusic_next", - "QQ音乐下一首", - mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - QQMusicNextHandler(context) - ) - - client.registerLocalFunction( - "qqmusic_previous", - "QQ音乐上一首", - mapOf( - "type" to "object", - "properties" to emptyMap(), - "required" to emptyList() - ), - QQMusicPreviousHandler(context) - ) - - Log.d(TAG, "QQ音乐所有功能注册完成") - } catch (e: Exception) { - Log.e(TAG, "注册QQ音乐功能失败: ${e.message}", e) - } - } -} - -// 新的仅搜索处理器(不包含播放功能) -private class QQMusicSearchOnlyHandler(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() - - // 使用 suspendCoroutine 将回调转换为协程,并添加防重复调用机制 - suspendCoroutine { continuation -> - var isResumed = false // 添加标志防止重复调用 - - qqMusicSingleton.searchSongs(keyword) { searchResult -> - // 检查是否已经恢复过协程 - if (isResumed) { - Log.w("QQMusicSearchOnlyHandler", "检测到重复回调,忽略") - return@searchSongs - } - - isResumed = true // 标记已处理 - - val result = if (searchResult.isSuccess) { - val songs = searchResult.getOrNull() ?: emptyList() - - // 根据搜索类型处理结果 - val processedSongs = when (searchType) { - 0 -> { // 歌曲搜索,返回所有结果 - songs - } - else -> { // 专辑、歌手等其他类型,返回所有结果 - songs - } - } - - val songsJson = processedSongs.joinToString(",") { song -> - val songMap = song as Map - // 添加调试日志 - Log.d("QQMusicSearchOnlyHandler", "原始数据: $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\"}" - } - - if (songs.isNotEmpty()) { - "{\"success\": true, \"message\": \"搜索成功\",\"iscard\": true,\"broadcast\": false, \"data\": [$songsJson]}" - } else { - "{\"success\": false, \"message\": \"未找到相关歌曲\"}" - } - } else { - val error = searchResult.exceptionOrNull() - "{\"success\": false, \"message\": \"搜索失败: ${error?.message}\"}" - } - - // 恢复协程执行 - continuation.resume(result) - } - } - } catch (e: Exception) { - Log.e("QQMusicSearchOnlyHandler", "QQ音乐搜索失败", e) - "{\"success\": false, \"message\": \"搜索异常: ${e.message}\"}" - } - } -} - -private class QQMusicPlaySongsHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - @Suppress("UNCHECKED_CAST") - val songMids = arguments["songMids"] as? List ?: return "{\"success\": false, \"message\": \"缺少歌曲ID列表\"}" - val index = (arguments["index"] as? Number)?.toInt() ?: 0 - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.playSongs(songMids, index) { playResult -> - result = if (playResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"开始播放歌曲列表\"}" - } else { - val error = playResult.exceptionOrNull() - "{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicPlaySongsHandler", "QQ音乐播放歌曲失败", e) - "{\"nocard\": true, \"success\": false, \"message\": \"播放异常: ${e.message}\"}" - } - } -} - -private class QQMusicPlayHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.playMusic { playResult -> - result = if (playResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"开始播放\"}" - } else { - val error = playResult.exceptionOrNull() - "{\"success\": false, \"message\": \"播放失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicPlayHandler", "QQ音乐播放失败", e) - "{\"success\": false, \"message\": \"播放异常: ${e.message}\"}" - } - } -} - -private class QQMusicPauseHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.pauseMusic { pauseResult -> - result = if (pauseResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"暂停播放\"}" - } else { - val error = pauseResult.exceptionOrNull() - "{\"success\": false, \"message\": \"暂停失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicPauseHandler", "QQ音乐暂停失败", e) - "{\"success\": false, \"message\": \"暂停异常: ${e.message}\"}" - } - } -} - -private class QQMusicResumeHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.resumeMusic { resumeResult -> - result = if (resumeResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"恢复播放\"}" - } else { - val error = resumeResult.exceptionOrNull() - "{\"success\": false, \"message\": \"恢复播放失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicResumeHandler", "QQ音乐恢复播放失败", e) - "{\"success\": false, \"message\": \"恢复播放异常: ${e.message}\"}" - } - } -} - -private class QQMusicStopHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.stopMusic { stopResult -> - result = if (stopResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"停止播放\"}" - } else { - val error = stopResult.exceptionOrNull() - "{\"success\": false, \"message\": \"停止失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicStopHandler", "QQ音乐停止失败", e) - "{\"success\": false, \"message\": \"停止异常: ${e.message}\"}" - } - } -} - -private class QQMusicNextHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.skipToNext { nextResult -> - result = if (nextResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"切换到下一首\"}" - } else { - val error = nextResult.exceptionOrNull() - "{\"success\": false, \"message\": \"切换失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicNextHandler", "QQ音乐下一首失败", e) - "{\"success\": false, \"message\": \"切换异常: ${e.message}\"}" - } - } -} - -private class QQMusicPreviousHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.skipToPrevious { previousResult -> - result = if (previousResult.isSuccess) { - "{\"broadcast\": false, \"success\": true, \"message\": \"切换到上一首\"}" - } else { - val error = previousResult.exceptionOrNull() - "{\"success\": false, \"message\": \"切换失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicPreviousHandler", "QQ音乐上一首失败", e) - "{\"success\": false, \"message\": \"切换异常: ${e.message}\"}" - } - } -} - -private class QQMusicGetPlaybackStateHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.getPlaybackState { stateResult -> - result = if (stateResult.isSuccess) { - val state = stateResult.getOrNull() ?: emptyMap() - val stateJson = state.entries.joinToString(",") { (key, value) -> - "\"$key\": \"$value\"" - } - "{\"broadcast\": false, \"success\": true, \"message\": \"获取播放状态成功\", \"data\": {$stateJson}}" - } else { - val error = stateResult.exceptionOrNull() - "{\"success\": false, \"message\": \"获取播放状态失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("CahtApi", "获取QQ音乐播放状态失败", e) - "{\"success\": false, \"message\": \"获取播放状态异常: ${e.message}\"}" - } - } -} - -private class QQMusicGetCurrentSongHandler(private val context: Context?) : FunctionHandler { - override suspend fun handle(arguments: Map): String { - return try { - val qqMusicSingleton = QQMusicSingleton.getInstance() - var result = "" - // 创建一个CompletableDeferred用于等待回调结果 - val deferred = CompletableDeferred() - qqMusicSingleton.getCurrentSong { songResult -> - result = if (songResult.isSuccess) { - val song = songResult.getOrNull() ?: emptyMap() - val songJson = song.entries.joinToString(",") { (key, value) -> - "\"$key\": \"$value\"" - } - "{\"success\": true, \"message\": \"获取当前歌曲成功\", \"data\": {$songJson}}" - } else { - val error = songResult.exceptionOrNull() - "{\"success\": false, \"message\": \"获取当前歌曲失败: ${error?.message}\"}" - } - // 回调完成,唤醒挂起的协程 - deferred.complete(result) - } - - // 挂起等待回调结果(无超时,直到回调触发) - deferred.await() - } catch (e: Exception) { - Log.e("QQMusicLoginHandler", "获取QQ音乐当前歌曲失败", e) - "{\"success\": false, \"message\": \"获取当前歌曲异常: ${e.message}\"}" - } - } -} - diff --git a/apps/client/local_plugins/chat_api/ios/chat_api/Package.swift b/apps/client/local_plugins/chat_api/ios/chat_api/Package.swift index 2e6a43d6..2c840536 100644 --- a/apps/client/local_plugins/chat_api/ios/chat_api/Package.swift +++ b/apps/client/local_plugins/chat_api/ios/chat_api/Package.swift @@ -19,7 +19,6 @@ let package = Package( .package(path: "../../../swift_sdk_local"), .package(path: "../../../location_service/ios/location_service"), .package(path: "../../../music_service/ios/music_service"), - .package(path: "../../../qq_music/ios/qq_music"), ], @@ -30,7 +29,6 @@ let package = Package( .product(name: "MCP", package: "swift_sdk_local"), .product(name: "OpenAI", package: "OpenAI"), .product(name: "music-service", package: "music_service"), - .product(name: "qq-music", package: "qq_music"), .product(name: "location-service", package: "location_service") ], exclude: [] diff --git a/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift b/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift index 27506841..9c524272 100644 --- a/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift +++ b/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift @@ -535,16 +535,9 @@ public class MCPClient { switch musicServiceType { case "WY": initializeMusicSystemFunction() // 更明确的命名 - case "QQ": - initializeQQMusicSystemFunction() default: - // 正确传入参数,并明确检查的是QQ音乐 - if QQMusicSystemFunctionHandler.isMusicAppInstalled() { - initializeQQMusicSystemFunction() - } else { - // 可以考虑是否需要检查网易云音乐是否安装 - initializeMusicSystemFunction() - } + // QQ 音乐已于 2026-09-24 下线,"QQ" 与未指定一律走网易/系统音乐 + initializeMusicSystemFunction() } } @@ -566,15 +559,6 @@ public class MCPClient { // 静默处理错误 } } - private func initializeQQMusicSystemFunction() { - do { - // 初始化QQ音乐功能 - let qqMusicHandler: QQMusicSystemFunctionHandler = QQMusicSystemFunctionHandler() - qqMusicHandler.registerAllFunctions(client: self) - } catch { - // 静默处理错误 - } - } public func connectToSSE(mcpConfigJson: String) async throws -> Bool { diff --git a/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift b/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift deleted file mode 100644 index cc5a9e04..00000000 --- a/apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift +++ /dev/null @@ -1,332 +0,0 @@ -import Foundation -import qq_music - -class QQMusicSystemFunctionHandler { - - // 静态方法:检查指定音乐应用是否安装 - static func isMusicAppInstalled() -> Bool { - // 检查QQ音乐是否安装 - guard QPlayAutoManager.shared.isQQMusicInstalled else { - print("QPlayAutoManager: QQ音乐未安装,无法处理URL") - return false - } - return true - } - - // 添加 registerAllFunctions 方法 - func registerAllFunctions(client: MCPClient) { - // 注册QQ音乐搜索和播放功能 - client.registerLocalFunction( - name: "qqmusic_searchandplay_songs", - description: "搜索并播放QQ音乐歌曲", - parameters: [ - "type": "object", - "properties": [ - "keyword": [ - "type": "string", - "description": "搜索关键词" - ] - ], - "required": ["keyword"] - ], - handler: QQMusicSearchHandler() - ) - - // 注册暂停功能 - client.registerLocalFunction( - name: "qqmusic_pause", - description: "暂停QQ音乐播放", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicPauseHandler() - ) - - // 注册恢复播放功能 - client.registerLocalFunction( - name: "qqmusic_resume", - description: "恢复QQ音乐播放", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicResumeHandler() - ) - - // 注册停止功能 - client.registerLocalFunction( - name: "qqmusic_stop", - description: "停止QQ音乐播放", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicStopHandler() - ) - - // 注册下一首功能 - client.registerLocalFunction( - name: "qqmusic_next", - description: "播放下一首歌曲", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicNextHandler() - ) - - // 注册上一首功能 - client.registerLocalFunction( - name: "qqmusic_previous", - description: "播放上一首歌曲", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicPreviousHandler() - ) - - // 注册获取播放状态功能 - client.registerLocalFunction( - name: "qqmusic_get_playback_state", - description: "获取QQ音乐播放状态", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicGetPlaybackStateHandler() - ) - - // 注册获取当前歌曲功能 - client.registerLocalFunction( - name: "qqmusic_get_current_song", - description: "获取当前播放的歌曲信息", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicGetCurrentSongHandler() - ) - - // 注册获取播放时间功能 - client.registerLocalFunction( - name: "qqmusic_get_play_time", - description: "获取播放时间信息", - parameters: [ - "type": "object", - "properties": [:] as [String: Any], - "required": [] as [String] - ], - handler: QQMusicGetPlayTimeHandler() - ) - } -} - -// MARK: - Private Handler Classes - -private class QQMusicSearchHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - guard let keyword = arguments["keyword"] as? String else { - let result: [String: Any] = ["success": false, "error": "Missing keyword parameter"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } - - return await withCheckedContinuation { continuation in - // 添加线程安全的恢复保护 - var isResumed = false - let resumeLock = NSLock() - - func safeResume(with result: String) { - resumeLock.lock() - defer { resumeLock.unlock() } - - if !isResumed { - isResumed = true - continuation.resume(returning: result) - } - } - - // 调用更新后的搜索接口,包含错误处理 - QPlayAutoManager.shared.search( - keyword: keyword, - searchType: .song, - completion: { items in - var result: [String: Any] - - if let items = items, !items.isEmpty { - let songs = items.map { item -> [String: Any] in - return [ - "mid": item.mid ?? "", - "name": item.name ?? "", - "artist": item.singer?.name ?? "", - "album": item.album ?? "", - "duration": item.duration, - "pic": item.coverUri ?? "" - ] - } - result = [ - "success": true, - "message": "Found \(items.count) songs", - "iscard": true, - "broadcast": false, - "data": songs - ] - } else { - result = ["success": false, "error": "No songs found"] - } - - do { - let jsonData = try JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" - safeResume(with: jsonString) - } catch { - safeResume(with: "{\"success\": false, \"error\": \"JSON serialization failed\"}") - } - }, - onError: { error in - // 处理搜索错误 - var errorMessage = "Search failed" - if let error = error { - errorMessage = "Search failed: \(error.localizedDescription)" - } - - let result: [String: Any] = [ - "success": false, - "error": errorMessage - ] - - do { - let jsonData = try JSONSerialization.data(withJSONObject: result) - let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" - safeResume(with: jsonString) - } catch { - safeResume(with: "{\"success\": false, \"error\": \"Search failed and JSON serialization failed\"}") - } - } - ) - } - } -} - -private class QQMusicPauseHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - QPlayAutoManager.shared.pause() - let result: [String: Any] = ["success": true,"broadcast": false, "message": "Music paused"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicResumeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - QPlayAutoManager.shared.play() - let result: [String: Any] = ["success": true,"broadcast": false, "message": "Music resumed"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicStopHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - QPlayAutoManager.shared.stop() - let result: [String: Any] = ["success": true,"broadcast": false, "message": "Music stopped"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicNextHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - QPlayAutoManager.shared.next() - let result: [String: Any] = ["success": true, "broadcast": false, "message": "Skipped to next song"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicPreviousHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - QPlayAutoManager.shared.previous() - let result: [String: Any] = ["success": true, "broadcast": false, "message": "Skipped to previous song"] - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicGetPlaybackStateHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let playState = QPlayAutoManager.shared.currentPlayState - let isConnected = QPlayAutoManager.shared.isConnected - - var state = "unknown" - switch playState { - case .playing: - state = "playing" - case .pause: - state = "paused" - case .stop: - state = "stopped" - default: - state = "unknown" - } - - let result: [String: Any] = [ - "success": true, - "state": state, - "connected": isConnected - ] - - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicGetCurrentSongHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - var result: [String: Any] - - if let currentSong = QPlayAutoManager.shared.currentSong { - result = [ - "success": true, - "song": [ - "id": currentSong.id ?? "", - "name": currentSong.name ?? "", - "artist": currentSong.singer?.name ?? "", - "album": currentSong.album ?? "", - "duration": currentSong.duration - ] - ] - } else { - result = ["success": false, "error": "No current song"] - } - - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} - -private class QQMusicGetPlayTimeHandler: FunctionHandler { - func handle(arguments: [String: Any]) async throws -> String { - let currentProgress = QPlayAutoManager.shared.currentProgress - let currentSong = QPlayAutoManager.shared.currentSong - let totalDuration = currentSong?.duration ?? 0 - - let result: [String: Any] = [ - "success": true, - "current_time": currentProgress, - "total_time": totalDuration - ] - - let jsonData = try JSONSerialization.data(withJSONObject: result) - return String(data: jsonData, encoding: .utf8) ?? "{}" - } -} diff --git a/apps/client/local_plugins/qq_music/.gitignore b/apps/client/local_plugins/qq_music/.gitignore deleted file mode 100644 index e7d347d9..00000000 --- a/apps/client/local_plugins/qq_music/.gitignore +++ /dev/null @@ -1,33 +0,0 @@ -# Miscellaneous -*.class -*.log -*.pyc -*.swp -.DS_Store -.atom/ -.build/ -.buildlog/ -.history -.svn/ -.swiftpm/ -migrate_working_dir/ - -# IntelliJ related -*.iml -*.ipr -*.iws -.idea/ - -# The .vscode folder contains launch configuration and tasks you configure in -# VS Code which you may wish to be included in version control, so this line -# is commented out by default. -#.vscode/ - -# Flutter/Dart/Pub related -# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. -/pubspec.lock -**/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -build/ diff --git a/apps/client/local_plugins/qq_music/.metadata b/apps/client/local_plugins/qq_music/.metadata deleted file mode 100644 index 709f51d7..00000000 --- a/apps/client/local_plugins/qq_music/.metadata +++ /dev/null @@ -1,33 +0,0 @@ -# This file tracks properties of this Flutter project. -# Used by Flutter tool to assess capabilities and perform upgrades etc. -# -# This file should be version controlled and should not be manually edited. - -version: - revision: "5c1433509f997e9059d230942bdf83f1b88a179e" - channel: "stable" - -project_type: plugin - -# Tracks metadata for the flutter migrate command -migration: - platforms: - - platform: root - create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - - platform: android - create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - - platform: ios - create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e - - # User provided section - - # List of Local paths (relative to this file) that should be - # ignored by the migrate tool. - # - # Files that are not part of the templates will be ignored by default. - unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/apps/client/local_plugins/qq_music/CHANGELOG.md b/apps/client/local_plugins/qq_music/CHANGELOG.md deleted file mode 100644 index 41cc7d81..00000000 --- a/apps/client/local_plugins/qq_music/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/apps/client/local_plugins/qq_music/LICENSE b/apps/client/local_plugins/qq_music/LICENSE deleted file mode 100644 index ba75c69f..00000000 --- a/apps/client/local_plugins/qq_music/LICENSE +++ /dev/null @@ -1 +0,0 @@ -TODO: Add your license here. diff --git a/apps/client/local_plugins/qq_music/README.md b/apps/client/local_plugins/qq_music/README.md deleted file mode 100644 index 96d01f18..00000000 --- a/apps/client/local_plugins/qq_music/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# qq_music - -A new Flutter project. - -## Getting Started - -This project is a starting point for a Flutter -[plug-in package](https://flutter.dev/to/develop-plugins), -a specialized package that includes platform-specific implementation code for -Android and/or iOS. - -For help getting started with Flutter development, view the -[online documentation](https://docs.flutter.dev), which offers tutorials, -samples, guidance on mobile development, and a full API reference. - diff --git a/apps/client/local_plugins/qq_music/analysis_options.yaml b/apps/client/local_plugins/qq_music/analysis_options.yaml deleted file mode 100644 index a5744c1c..00000000 --- a/apps/client/local_plugins/qq_music/analysis_options.yaml +++ /dev/null @@ -1,4 +0,0 @@ -include: package:flutter_lints/flutter.yaml - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options diff --git a/apps/client/local_plugins/qq_music/android/.gitignore b/apps/client/local_plugins/qq_music/android/.gitignore deleted file mode 100644 index 161bdcda..00000000 --- a/apps/client/local_plugins/qq_music/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.cxx diff --git a/apps/client/local_plugins/qq_music/android/build.gradle b/apps/client/local_plugins/qq_music/android/build.gradle deleted file mode 100644 index 1a579ecf..00000000 --- a/apps/client/local_plugins/qq_music/android/build.gradle +++ /dev/null @@ -1,79 +0,0 @@ -group = "com.yunqiinnovation.qq_music" -version = "1.0-SNAPSHOT" - -buildscript { - ext.kotlin_version = "2.1.0" - repositories { - google() - mavenCentral() - } - - dependencies { - classpath("com.android.tools.build:gradle:8.7.3") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") - } -} - -allprojects { - repositories { - google() - mavenCentral() - // 添加本地AAR仓库 - flatDir { - dirs 'libs' - } - } -} - -apply plugin: "com.android.library" -apply plugin: "kotlin-android" - -android { - namespace = "com.yunqiinnovation.qq_music" - - compileSdk = 35 - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11 - } - - // 合并后的 sourceSets 配置 - sourceSets { - main { - java.srcDirs += "src/main/kotlin" - aidl.srcDirs = ['src/main/aidl'] - } - test { - java.srcDirs += "src/test/kotlin" - } - } - - defaultConfig { - minSdk = 21 - } - - buildFeatures { - aidl true - } - - dependencies { - testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.0.0") - - // 添加AndroidX AppCompat依赖 - implementation 'androidx.appcompat:appcompat:1.6.1' - - // 添加 Gson 依赖 - implementation 'com.google.code.gson:gson:2.10.1' - - // 添加 ConstraintLayout 依赖 - implementation 'androidx.constraintlayout:constraintlayout:2.1.4' - - api(name: 'qqmusic-innovation-aidl-api-sdk-1.0.26', ext: 'aar') - } -} diff --git a/apps/client/local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar b/apps/client/local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar deleted file mode 100644 index aba449c1..00000000 Binary files a/apps/client/local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar and /dev/null differ diff --git a/apps/client/local_plugins/qq_music/android/settings.gradle b/apps/client/local_plugins/qq_music/android/settings.gradle deleted file mode 100644 index 135ddb68..00000000 --- a/apps/client/local_plugins/qq_music/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'qq_music' diff --git a/apps/client/local_plugins/qq_music/android/src/main/AndroidManifest.xml b/apps/client/local_plugins/qq_music/android/src/main/AndroidManifest.xml deleted file mode 100644 index 64a0aad0..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - diff --git a/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl b/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl deleted file mode 100644 index 718a193a..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl +++ /dev/null @@ -1,15 +0,0 @@ -// IPlayService.aidl -package com.yunqiinnovation.qq_music; - -// Declare any non-default types here with import statements - -import com.yunqiinnovation.qq_music.IPrint; - -interface IPlayService { - void bindService(); - void startPcmMode(); - void stopPcmMode(); - void resumeOrPause(); - void playNext(); - void setPrintMessageCallback(IPrint callback); -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl b/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl deleted file mode 100644 index 70d14f41..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl +++ /dev/null @@ -1,8 +0,0 @@ -// IPrintMessage.aidl -package com.yunqiinnovation.qq_music; - -// Declare any non-default types here with import statements - -interface IPrint { - void print(String msg); -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java deleted file mode 100644 index b27ae44e..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.yunqiinnovation.qq_music; - -import com.tencent.qqmusic.third.api.contract.CommonCmd; - -public class Config { - - /** - * 需向QQ音乐发邮件申请,具体参考README - */ - public static final String OPENID_APPID = "2000000921"; - - /** - * 配置接入方自己生成的RSA私钥,由于是Demo,所以直接写在代码中了,真实环境中需要注意私钥泄漏的风险。 - */ - public static final String OPENID_APP_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + - "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALWuTayxg0296yfm\n" + - "h8WhunybY1BEFt9fB4qb3TGvB0vkd+oKDoBdiTevtgwSL47h4O0ISNTQqqmVOvk1\n" + - "EQMGcLESBEdHquPX2uOplwmmVWx1un9NYB4JKaHmUkCXOIoiCgmEY052zFY2T+a0\n" + - "RfFiaUTc0wRMynquE8Qp18i23IUBAgMBAAECgYBf/s1D5jd+qE7MMl6i6iVjGaeM\n" + - "Da4YMFoHvvtM+TJdw0Yo/09SPBn4tnr2PdWrJWZ3V91DtU23dQZIDuYCNzDrOkVr\n" + - "93W5Pkqprdbde6qRhWm1VTZVN7h85DStGBzovSxQ6DQNHZFzIriJkfGDjPopmIil\n" + - "awQvMPlS3lLYkzHwAQJBAOWBlshEMHXeqXekjJExwX7YSy2oabWXtllCFbcPprAY\n" + - "MiM8cdNsEhnhrsmdBN4crqTQKJq/ATaYFhzASuIiZ2ECQQDKp17+A3u6q7RZ3Vmb\n" + - "khvAATQpVK5loCOzuyPU7/UGU49AO1DIaPwDvaUqL6PuF9eFrcRHA5AFbBjOgYNn\n" + - "piGhAkEAr5gMo0D4ytYHVxDAs74by071hgNajaN70qLrNlp1MDny8l6mwBnpZ3/y\n" + - "WTMXrFDJljPlPTByXzDRikHdiVLFQQJATMOTpxK4tqA+A+BmBhkVa+jI0nMiUJqV\n" + - "i4py60EuBoR2efU+B3xFIkGfI9jB9vmPCTMma2P9FSC6UNccaJG0oQJBAJYb6Ch9\n" + - "yowGPpY1CShaR+TfauUreJBd4PSpu8mFlYUssp2AXYcJM51RMzrZKXvTTOvwkStc\n" + - "NRCBcPVAYcVUnXo=\n" + - "-----END PRIVATE KEY-----"; - - /** - * 获取纯Base64格式的私钥(去除PEM头尾标记) - * @return 纯Base64格式的私钥字符串 - */ - public static String getBase64PrivateKey() { - return OPENID_APP_PRIVATE_KEY - .replace("-----BEGIN PRIVATE KEY-----", "") - .replace("-----END PRIVATE KEY-----", "") - .replaceAll("\\s+", ""); // 移除所有空白字符(包括换行符) - } - - /** - * 配置平台类型,可选值如下: - * {@link CommonCmd#AIDL_PLATFORM_TYPE_PHONE } - * {@link CommonCmd#AIDL_PLATFORM_TYPE_TV} - * {@link CommonCmd#AIDL_PLATFORM_TYPE_CAR} - * {@link CommonCmd#AIDL_PLATFORM_TYPE_PAD} - */ - // 修改这一行:使用CommonCmd中的常量而不是字符串 - public static String BIND_PLATFORM = CommonCmd.AIDL_PLATFORM_TYPE_PHONE; - -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt deleted file mode 100644 index 049dbb67..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt +++ /dev/null @@ -1,306 +0,0 @@ -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 -import android.os.Looper -import android.util.Log -import com.yunqiinnovation.qq_music.openid.OpenIDHelper -import com.yunqiinnovation.qq_music.util.QPlayBindHelper -import com.yunqiinnovation.qq_music.util.QQMusicApiWrapper -import com.tencent.qqmusic.third.api.contract.* -import org.json.JSONObject -import org.json.JSONTokener - -/** - * QQ音乐登录管理器 - * 负责处理QQ音乐的登录授权流程 - */ -class QQMusicLoginManager(private val context: Context) { - - companion object { - private const val TAG = "QQMusicLoginManager" - private const val BASE_SCHEME = "eaimar://" - private const val URI_LOGIN = BASE_SCHEME + "login" - - // 定义错误码常量,替代 LoginExampleActivity 中的常量 - const val LOGIN_FAILED = 1 - } - private var isBindQQMusicService = false - private lateinit var qPlayBindHelper: QPlayBindHelper - private var qqMusicApi: IQQMusicApi? = null - private lateinit var qqMusicApiWrapper: QQMusicApiWrapper - private val handler = Handler(Looper.getMainLooper()) - - /** - * 登录结果回调接口 - */ - interface LoginCallback { - fun onSuccess() - fun onError(errorCode: String, errorMessage: String) - } - - //是否连接 - public val isLogined: Boolean - get() = isBindQQMusicService && qPlayBindHelper != null - - - /** - * 开始登录流程 - */ - 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 -> { - // 已授权,直接请求登录信息 - Log.d(TAG, "已授权,直接获取登录信息") -// isBindQQMusicService = true - startAuthProcess(callback) - } - ErrorCodes.ERROR_API_NO_PERMISSION -> { - // 需要授权 - Log.d(TAG, "需要授权,启动授权流程") - startAuthProcess(callback) - } - else -> { - isBindQQMusicService = false - Log.e(TAG, "检查授权状态失败: $code") - callback.onError("AUTH_CHECK_FAILED", "检查授权状态失败") - } - } - } catch (e: Exception) { - Log.e(TAG, "检查授权状态异常", e) - isBindQQMusicService = false - callback.onError("AUTH_CHECK_ERROR", "检查授权状态异常: ${e.message}") - } - } - - /** - * 启动授权流程 - */ - fun startAuthProcess(callback: LoginCallback) { - Log.d(TAG, "启动QQ音乐授权流程") - - // 启动QQ音乐登录 - CommonCmd.loginQQMusic(context,context.packageName, URI_LOGIN) - - // 等待用户登录完成后进行身份验证 - handler.postDelayed({ - verifyIdentity(callback) - }, 3000) // 给用户足够的登录时间 - } - - /** - * 启动授权流程 - */ - 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) - 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 - } - /** - * 启动授权流程 - */ - public fun openQQMusic() { - Log.d(TAG, "启动QQ音乐登录页面") - // 启动QQ音乐登录 - CommonCmd.startQQMusicProcess(context,context.packageName) - - } - - /** - * 验证身份 - */ - private fun verifyIdentity(callback: LoginCallback) { - Log.d(TAG, "开始身份验证") - - val time = System.currentTimeMillis() - val nonce = time.toString() - val encryptString = OpenIDHelper.getEncryptString(nonce) - - CommonCmd.verifyCallerIdentity( - context, - Config.OPENID_APPID, - context.packageName, - encryptString, - BASE_SCHEME - ) - - // 延迟后请求授权 - handler.postDelayed({ - requestAuth(callback) - }, 2000) - } - - /** - * 请求授权并获取登录信息 - */ - private fun requestAuth(callback: LoginCallback) { - Log.d(TAG, "开始请求授权") - - qqMusicApiWrapper.requestAuthNew( - success = { encrypt -> - Log.d(TAG, "授权成功,开始解密登录信息") - parseLoginResult(encrypt, callback) - }, - failed = { errorCode, errorMsg -> - Log.e(TAG, "授权失败: code=$errorCode, msg=$errorMsg") - isBindQQMusicService = false - val errorMessage = when (errorCode) { - LOGIN_FAILED -> "登录失败: $errorMsg" - else -> "授权失败: $errorMsg" - } - - callback.onError("AUTH_FAILED", errorMessage) - } - ) - } - - /** - * 解析登录结果 - */ - private fun parseLoginResult(encrypt: String, callback: LoginCallback) { - try { - val qmDecryptString = OpenIDHelper.decryptQQMEncryptString(encrypt) - if (qmDecryptString != null) { - val appParser = JSONTokener(qmDecryptString) - val appDecryptJson = appParser.nextValue() as JSONObject - val sign = appDecryptJson.getString(Keys.API_RETURN_KEY_SIGN) - val returnNonce = appDecryptJson.getString(Keys.API_RETURN_KEY_NONCE) - - // 检查签名 - if (OpenIDHelper.checkQMSign(sign, returnNonce)) { -// val openId = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_ID) -// val openToken = appDecryptJson.getString(Keys.API_RETURN_KEY_OPEN_TOKEN) -// val expireTime = appDecryptJson.getString(Keys.API_PARAM_KEY_SDK_EXPIRETIME) -// Log.d(TAG, "登录成功: openId=$openId") - isBindQQMusicService = true - handler.post { - callback.onSuccess() - } - } else { - isBindQQMusicService = false - Log.e(TAG, "签名验证失败") - handler.post { - callback.onError("SIGN_ERROR", "签名验证失败") - } - } - } else { - isBindQQMusicService = false - Log.e(TAG, "解密失败") - handler.post { - callback.onError("DECRYPT_ERROR", "解密失败") - } - } - } catch (e: Exception) { - Log.e(TAG, "解析登录结果失败", e) - isBindQQMusicService = false - handler.post { - callback.onError("PARSE_ERROR", "解析登录结果失败: ${e.message}") - } - } - } - - /** - * 清理资源 - */ - fun cleanup() { - try { - if (::qPlayBindHelper.isInitialized) { - qPlayBindHelper.unBindQQMusic() - } - } catch (e: Exception) { - Log.e(TAG, "清理资源失败", e) - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt deleted file mode 100644 index 62690295..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt +++ /dev/null @@ -1,726 +0,0 @@ -package com.yunqiinnovation.qq_music - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.content.pm.PackageManager -import android.os.Bundle -import android.os.IBinder -import android.util.Log -import com.google.gson.Gson -import com.google.gson.JsonParser -import com.tencent.qqmusic.third.api.contract.* -import android.os.Handler -import android.os.Looper - -class QQMusicManager(private val context: Context) { - companion object { - const val TAG = "QQMusicManager" - } - - private var qqmusicApi: IQQMusicApi? = null - private var isBindQQMusicService = false - private val gson = Gson() - - // 回调接口 - interface SearchCallback { - fun onSuccess(songs: List>) - fun onError(errorCode: Int, errorMessage: String) - } - - interface PlayCallback { - fun onSuccess() - fun onError(errorCode: Int, errorMessage: String) - } - - interface InfoCallback { - fun onSuccess(data: Map) - fun onError(errorCode: Int, errorMessage: String) - } - - // 新增:绑定服务的回调接口 - interface BindCallback { - fun onSuccess() - fun onError(errorCode: Int, errorMessage: String) - } - - - init { - // bindQQMusicApiService() - } - - //是否连接 - public val isConnected: Boolean - get() = isBindQQMusicService && qqmusicApi != null - - private fun sayHi() { - if (!isBindQQMusicService || qqmusicApi == null) { - Log.w(TAG, "连接QQ音乐失败,可能是QQ音乐帐号状态异常,请打开QQ音乐检查帐号状态是否异常?") - return - } - - try { - val bundle = Bundle() - bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) - bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) - - val result = qqmusicApi?.execute("hi", bundle) - Log.d(TAG, "sayHi ret:" + result?.getInt(Keys.API_RETURN_KEY_CODE)) - } catch (e: Exception) { - Log.e(TAG, "发送hi消息失败", e) - // 如果是DeadObjectException,尝试重新绑定服务 - if (e is android.os.DeadObjectException) { - Log.w(TAG, "检测到DeadObjectException,重新绑定服务") - qqmusicApi = null - isBindQQMusicService = false - - // 延迟重新绑定 - Handler(Looper.getMainLooper()).postDelayed({ - bindQQMusicApiService() - }, 1000) - } - } - } - - fun bindQQMusicApiService(callback: BindCallback? = null) { - Log.d(TAG, "开始绑定QQ音乐服务,平台类型: ${Config.BIND_PLATFORM}") - - val intent = when (Config.BIND_PLATFORM) { - CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> { - Intent("com.tencent.qqmusic.third.api.QQMusicApiService").apply { - `package` = "com.tencent.qqmusic" - } - } - CommonCmd.AIDL_PLATFORM_TYPE_CAR -> { - Intent("com.tencent.qqmusiccar.third.api.QQMusicApiService").apply { - `package` = "com.tencent.qqmusiccar" - } - } - CommonCmd.AIDL_PLATFORM_TYPE_TV -> { - Intent("com.tencent.qqmusictv.third.api.QQMusicApiService").apply { - `package` = "com.tencent.qqmusictv" - } - } - else -> { - Log.e(TAG, "不支持的平台类型: ${Config.BIND_PLATFORM}") - callback?.onError(-1, "不支持的平台类型: ${Config.BIND_PLATFORM}") - return - } - } - - Log.d(TAG, "绑定Intent: action=${intent.action}, package=${intent.`package`}") - - try { - val bindResult = context.bindService(intent, object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - Log.d(TAG, "QQ音乐服务连接成功") - qqmusicApi = IQQMusicApi.Stub.asInterface(service) - isBindQQMusicService = true - - // 初始化SDK - CommonCmd.init(Config.BIND_PLATFORM) - - // 延迟发送hi消息,确保Binder连接稳定 - Handler(Looper.getMainLooper()).postDelayed({ - sayHi() -// // 绑定成功后调用回调 -// callback?.onSuccess() - }, 500) // 延迟500ms - } - - override fun onServiceDisconnected(name: ComponentName?) { - Log.d(TAG, "QQ音乐服务连接断开") - qqmusicApi = null - isBindQQMusicService = false - // 服务断开时通知错误 -// callback?.onError(-2, "QQ音乐服务连接断开") - //重连绑定 -// bindQQMusicApiService(callback) - } - }, Context.BIND_AUTO_CREATE) - - Log.d(TAG, "bindService结果: $bindResult") - - if (!bindResult) { - Log.e(TAG, "绑定QQ音乐服务失败,尝试启动QQ音乐进程") - // 尝试启动QQ音乐进程 - CommonCmd.startQQMusicProcess(context, context.packageName) - - // 延迟重试绑定 - Handler(Looper.getMainLooper()).postDelayed({ - val retryResult = context.bindService(intent, object : ServiceConnection { - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - Log.d(TAG, "QQ音乐服务连接成功") - qqmusicApi = IQQMusicApi.Stub.asInterface(service) - isBindQQMusicService = true - // 初始化SDK - CommonCmd.init(Config.BIND_PLATFORM) - // 延迟发送hi消息,确保Binder连接稳定 - Handler(Looper.getMainLooper()).postDelayed({ - sayHi() - }, 500) // 延迟500ms - } - override fun onServiceDisconnected(name: ComponentName?) { - Log.d(TAG, "QQ音乐服务连接断开") - qqmusicApi = null - isBindQQMusicService = false - - } - }, Context.BIND_AUTO_CREATE) - Log.d(TAG, "重试绑定QQ音乐服务 $retryResult") - if (!retryResult) { - callback?.onError(-3, "重试绑定QQ音乐服务失败") - }else{ - Handler(Looper.getMainLooper()).postDelayed({ - callback?.onSuccess() - }, 1000) // 延迟500ms - } - }, 2000) - }else{ - Handler(Looper.getMainLooper()).postDelayed({ - callback?.onSuccess() - }, 1000) // 延迟500ms - - } - } catch (e: Exception) { - Log.e(TAG, "绑定QQ音乐服务异常", e) - callback?.onError(-4, "绑定QQ音乐服务异常: ${e.message}") - } - } - - /** - * 搜索歌曲 - * @param keyword 搜索关键词 - * @param searchType 搜索类型,参考Data.SearchType - * @param callback 回调 - */ - fun searchSongs( - keyword: String, - searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, - callback: SearchCallback - ) { - Log.d(TAG, "searchSongs: keyword=$keyword, searchType=$searchType") - - if (!isBindQQMusicService || qqmusicApi == null) { - Log.e(TAG, "QQ音乐服务未连接") - callback.onError(-1, "QQ音乐服务未连接") - return - } - - // 先测试服务连接状态 - try { - val testBundle = Bundle() - testBundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) - testBundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) - - val testResult = qqmusicApi?.execute("hi", testBundle) - val testCode = testResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - Log.d(TAG, "服务连接测试结果: $testCode") - - if (testCode != ErrorCodes.ERROR_OK) { - Log.e(TAG, "QQ音乐服务连接异常: $testCode") - val testErrorMessage = when (testCode) { - ErrorCodes.ERROR_API_NO_PERMISSION -> "没有权限访问QQ音乐服务" - ErrorCodes.ERROR_API_NOT_INITIALIZED -> "QQ音乐API未初始化" - else -> "QQ音乐服务连接异常 (错误码: $testCode)" - } - callback.onError(testCode, testErrorMessage) - return - } - } catch (e: Exception) { - Log.e(TAG, "测试服务连接失败", e) - callback.onError(-1, "测试服务连接失败: ${e.message}") - return - } - - val params = Bundle().apply { - putInt("searchType", searchType) - putString("keyword", keyword) - putBoolean("firstPage", true) - } - - Log.d(TAG, "调用QQ音乐搜索API,参数: searchType=$searchType, keyword=$keyword") - - try { - // 直接调用executeAsync,不接收返回值 - qqmusicApi?.executeAsync("search", params, object : IQQMusicApiCallback.Stub() { - override fun onReturn(result: Bundle?) { - Log.d(TAG, "搜索API返回结果") - - if (result == null) { - Log.e(TAG, "搜索返回结果为null") - callback.onError(-1, "搜索返回结果为null") - return - } - - val code = result.getInt(Keys.API_RETURN_KEY_CODE, -1) - Log.d(TAG, "搜索返回码: $code") - - if (code == ErrorCodes.ERROR_OK) { - try { - val dataJson = result.getString(Keys.API_RETURN_KEY_DATA) - Log.d(TAG, "搜索数据: $dataJson") - - if (dataJson != null && dataJson.isNotEmpty()) { - val array = JsonParser().parse(dataJson).asJsonArray - val songs = mutableListOf>() - - for (elem in array) { - val song = gson.fromJson(elem, Data.Song::class.java) - songs.add(mapOf( - "mid" to song.mid, - "id" to song.id, - "name" to song.title, - "singer" to song.singer?.title, - "album" to song.album?.title, - "duration" to song.duration, - "pic" to song.album?.coverUri - )) - } - Log.d(TAG, "解析完成,共 ${songs.size} 首歌曲") - callback.onSuccess(songs) - } else { - Log.e(TAG, "搜索结果数据为空") - callback.onError(code, "搜索结果数据为空") - } - } catch (e: Exception) { - Log.e(TAG, "解析搜索结果异常", e) - callback.onError(-1, "解析搜索结果失败: ${e.message}") - } - } else { - val error = result.getString(Keys.API_RETURN_KEY_ERROR) ?: "搜索失败" - Log.e(TAG, "搜索失败: code=$code, error=$error") - - // 根据实际可用的错误码提供具体的错误信息 - val errorMessage = when (code) { - ErrorCodes.ERROR_API_NO_PERMISSION -> "没有搜索权限,请检查应用授权" - ErrorCodes.ERROR_API_NOT_INITIALIZED -> "QQ音乐API未初始化,请重新登录" - else -> "搜索失败: $error (错误码: $code)" - } - callback.onError(code, errorMessage) - } - } - }) - - Log.d(TAG, "executeAsync调用完成,等待回调") - - } catch (e: Exception) { - Log.e(TAG, "调用搜索API异常", e) - callback.onError(-1, "调用搜索API异常: ${e.message}") - } - } - - /** - * 播放歌曲列表 - * @param songMids 歌曲MID列表 - * @param index 开始播放的索引 - * @param callback 回调 - */ - fun playSongs( - songMids: List, - index: Int = 0, - callback: PlayCallback - ) { - - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val params = Bundle().apply { - putStringArrayList("midList", ArrayList(songMids)) - putInt("index", index) - } - // 直接调用executeAsync,不接收返回值 - qqmusicApi?.executeAsync("playSongMidAtIndex", params, object : IQQMusicApiCallback.Stub() { - override fun onReturn(result: Bundle?) { - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - val error = result?.getString(Keys.API_RETURN_KEY_ERROR) ?: "播放失败" - callback.onError(code, error) - } - } - }) - } - - /** - * 播放音乐 - */ - fun playMusic(callback: PlayCallback) { - - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("playMusic", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "播放失败") - } - } - - fun autopauseMusic(callback: PlayCallback){ - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - // 获取播放状态 - val stateResult = qqmusicApi?.execute("getPlaybackState", null) - val stateCode = stateResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - // 处理播放状态 - 使用正确的常量 - if (stateCode == ErrorCodes.ERROR_OK) { - val playState = stateResult?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 - val isPlaying = when (playState) { - PlayState.STARTED -> true - PlayState.PREPARED -> true - PlayState.BUFFERING -> true - PlayState.PAUSED -> false - PlayState.STOPPED -> false - PlayState.ERROR -> false - else -> false - } - if(isPlaying){ - val result = qqmusicApi?.execute("pauseMusic", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "暂停失败") - } - }else { - callback.onError(-1, "未播放") - } - } else { - callback.onError(-1, "未播放") - } - } - - /** - * 暂停音乐 - */ - fun pauseMusic(callback: PlayCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("pauseMusic", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "暂停失败") - } - } - - /** - * 恢复播放 - */ - fun resumeMusic(callback: PlayCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("resumeMusic", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "恢复播放失败") - } - } - - /** - * 停止音乐 - */ - fun stopMusic(callback: PlayCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("stopMusic", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "停止失败") - } - } - - /** - * 下一首 - */ - fun skipToNext(callback: PlayCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("skipToNext", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "切换下一首失败") - } - } - - /** - * 上一首 - */ - fun skipToPrevious(callback: PlayCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("skipToPrevious", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - callback.onSuccess() - } else { - callback.onError(code, "切换上一首失败") - } - } - - /** - * 同步当前播放信息 - 一次性获取所有播放状态 - */ - fun syncCurrentPlayInfo(callback: InfoCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - Log.w(TAG, "QQ音乐服务未连接,无法同步播放信息") - callback.onError(-1, "QQ音乐服务未连接") - return - } - - try { - Log.d(TAG, "开始同步当前播放信息") - - // 获取当前歌曲信息 - val songResult = qqmusicApi?.execute("getCurrentSong", null) - val songCode = songResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - // 获取播放状态 - val stateResult = qqmusicApi?.execute("getPlaybackState", null) - val stateCode = stateResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - // 获取当前播放时间 - val currTimeResult = qqmusicApi?.execute("getCurrTime", null) - val currTimeCode = currTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - // 获取总时长 - val totalTimeResult = qqmusicApi?.execute("getTotalTime", null) - val totalTimeCode = totalTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - Log.d(TAG, "API调用结果 - song: $songCode, state: $stateCode, currTime: $currTimeCode, totalTime: $totalTimeCode") - - // 构建返回数据 - val syncData = mutableMapOf() - - // 处理歌曲信息 - if (songCode == ErrorCodes.ERROR_OK) { - val songJson = songResult?.getString(Keys.API_RETURN_KEY_DATA) - if (!songJson.isNullOrEmpty()) { - try { - val jsonElement = JsonParser.parseString(songJson) - if (jsonElement.isJsonObject) { - val songData = jsonElement.asJsonObject - syncData["currentSong"] = mapOf( - "id" to (songData.get("id")?.asString ?: ""), - "mid" to (songData.get("mid")?.asString ?: ""), - "name" to (songData.get("title")?.asString ?: ""), - "singer" to (songData.get("singer")?.asJsonObject?.get("title")?.asString ?: ""), - "album" to (songData.get("album")?.asJsonObject?.get("title")?.asString ?: ""), - "duration" to (songData.get("duration")?.asLong ?: 0L), - "coverUri" to (songData.get("album")?.asJsonObject?.get("coverUri")?.asString ?: "") - ) - Log.d(TAG, "歌曲信息同步成功: ${songData.get("title")?.asString}") - } else { - Log.w(TAG, "返回的JSON不是有效的对象格式: $songJson") - syncData["currentSong"] = null - } - } catch (e: Exception) { - Log.e(TAG, "解析歌曲信息失败", e) - syncData["currentSong"] = null - } - } else { - syncData["currentSong"] = null - } - } else { - syncData["currentSong"] = null - } - - // 处理播放状态 - 使用正确的常量 - if (stateCode == ErrorCodes.ERROR_OK) { - val playState = stateResult?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 - val isPlaying = when (playState) { - PlayState.STARTED -> true - PlayState.PREPARED -> true - PlayState.BUFFERING -> true - PlayState.PAUSED -> false - PlayState.STOPPED -> false - PlayState.ERROR -> false - else -> false - } - syncData["isPlaying"] = isPlaying - syncData["playState"] = playState - Log.d(TAG, "播放状态同步成功: $playState (isPlaying: $isPlaying)") - } else { - syncData["isPlaying"] = false - syncData["playState"] = PlayState.STOPPED - } - - // 处理播放时间 - if (currTimeCode == ErrorCodes.ERROR_OK && totalTimeCode == ErrorCodes.ERROR_OK) { - val currentTime = (currTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 - val totalTime = (totalTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 - - syncData["currentTime"] = currentTime - syncData["totalTime"] = totalTime - - Log.d(TAG, "播放时间同步成功: ${currentTime}s / ${totalTime}s") - } else { - syncData["currentTime"] = 0L - syncData["totalTime"] = 0L - } - - // 添加同步时间戳 - syncData["syncTimestamp"] = System.currentTimeMillis() - - Log.d(TAG, "播放信息同步完成") - callback.onSuccess(syncData) - - } catch (e: Exception) { - Log.e(TAG, "同步播放信息异常", e) - callback.onError(-1, "同步播放信息异常: ${e.message}") - } - } - - /** - * 获取播放状态 - */ - fun getPlaybackState(callback: InfoCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("getPlaybackState", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - val state = result?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 - callback.onSuccess(mapOf("state" to state)) - } else { - callback.onError(code, "获取播放状态失败") - } - } - - /** - * 获取当前播放歌曲 - */ - fun getCurrentSong(callback: InfoCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - val result = qqmusicApi?.execute("getCurrentSong", null) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - if (code == ErrorCodes.ERROR_OK) { - try { - val curSongJson = result?.getString(Keys.API_RETURN_KEY_DATA) - if (curSongJson != null) { - val song = gson.fromJson(curSongJson, Data.Song::class.java) - val songMap = mapOf( - "mid" to song.mid, - "id" to song.id, - "name" to song.title, - "singer" to song.singer?.title, - "album" to song.album?.title, - "duration" to song.duration, - "pic" to song.album?.coverUri - ) - callback.onSuccess(songMap) - } else { - callback.onError(-1, "当前没有播放歌曲") - } - } catch (e: Exception) { - Log.e(TAG, "解析当前歌曲信息异常", e) - callback.onError(-1, "解析歌曲信息失败: ${e.message}") - } - } else { - callback.onError(code, "获取当前歌曲失败") - } - } - - /** - * 获取播放时长 - */ - fun getPlayTime(callback: InfoCallback) { - if (!isBindQQMusicService || qqmusicApi == null) { - callback.onError(-1, "QQ音乐服务未连接") - return - } - - try { - // 获取当前播放时间 - val currResult = qqmusicApi?.execute("getCurrTime", null) - val currCode = currResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - // 获取总时长 - val totalResult = qqmusicApi?.execute("getTotalTime", null) - val totalCode = totalResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - - if (currCode == ErrorCodes.ERROR_OK && totalCode == ErrorCodes.ERROR_OK) { - val currentTime = (currResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 - val totalTime = (totalResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 - - callback.onSuccess(mapOf( - "currentTime" to currentTime, - "totalTime" to totalTime - )) - } else { - callback.onError(-1, "获取播放时长失败") - } - } catch (e: Exception) { - Log.e(TAG, "获取播放时长异常", e) - callback.onError(-1, "获取播放时长异常: ${e.message}") - } - } - - /** - * 清理资源 - */ - fun cleanup() { - try { - if (isBindQQMusicService) { - isBindQQMusicService = false - Log.d(TAG, "QQ音乐服务已解绑") - } - } catch (e: Exception) { - Log.e(TAG, "解绑QQ音乐服务失败", e) - } - } - - /** - * 获取当前平台对应的QQ音乐包名 - */ - private fun getQQMusicPackageName(): String { - return when (Config.BIND_PLATFORM) { - CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic" - CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar" - CommonCmd.AIDL_PLATFORM_TYPE_TV -> "com.tencent.qqmusictv" - else -> "com.tencent.qqmusic" - } - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt deleted file mode 100644 index f0fee914..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt +++ /dev/null @@ -1,690 +0,0 @@ -package com.yunqiinnovation.qq_music - -import android.app.Activity -import android.content.Context -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.os.Handler -import android.os.Looper -import android.util.Log -import com.tencent.qqmusic.third.api.contract.* - -/** - * QQ音乐单例管理器 - * 提供统一的音乐播放接口,可以被Plugin和MCP Client共同使用 - */ -class QQMusicSingleton private constructor() { - - companion object { - private const val TAG = "QQMusicSingleton" - private const val PREF_NAME = "qq_music_login" - private const val KEY_OPEN_ID = "open_id" - private const val KEY_OPEN_TOKEN = "open_token" - private const val KEY_EXPIRE_TIME = "expire_time" - - @Volatile - private var INSTANCE: QQMusicSingleton? = null - - fun getInstance(): QQMusicSingleton { - return INSTANCE ?: synchronized(this) { - INSTANCE ?: QQMusicSingleton().also { INSTANCE = it } - } - } - } - - private var context: Context? = null - private var loginManager: QQMusicLoginManager? = null - private var musicManager: QQMusicManager? = null - private val handler = Handler(Looper.getMainLooper()) - private var sharedPreferences: SharedPreferences? = null - private var currentLoginResult: LoginResult? = null - // 添加搜索超时跟踪 - private val searchTimeouts = mutableMapOf() - - /** - * 初始化单例,设置Activity上下文 - */ - fun initialize(context: Context) { - this.context = context - // this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) - // loadSavedLoginInfo() - // Log.d(TAG, "QQ音乐单例初始化完成") - } - /** - * 检查QQ音乐App是否已安装 - */ - fun isQQMusicInstalled(): Boolean { - val packageName = when (Config.BIND_PLATFORM) { - CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic" - CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar" - CommonCmd.AIDL_PLATFORM_TYPE_TV -> "com.tencent.qqmusictv" - else -> { - Log.e(TAG, "不支持的平台类型: ${Config.BIND_PLATFORM}") - return false - } - } - - return try { - this.context?.packageManager?.getPackageInfo(packageName, 0) - Log.d(TAG, "QQ音乐app已安装: $packageName") - true - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "QQ音乐app未安装: $packageName") - false - } catch (e: Exception) { - Log.e(TAG, "检查QQ音乐app安装状态失败", e) - false - } - } - /** - * 检查是否已登录且未过期 - */ - fun isLoggedIn(): Boolean { - return loginManager != null && loginManager!!.isLogined - } - - /** - * 登录接口 - */ - fun login(callback: (Result) -> Unit) { - if (!isQQMusicInstalled()){ - callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) - return - } - val currentContext = context - // 先检查是否已经登录且未过期 - if (isLoggedIn()) { - Log.d(TAG, "用户已登录且未过期,直接返回登录信息") - ensureMusicManager(currentContext!!,object: QQMusicManager. BindCallback{ - override fun onSuccess() { - callback(Result.success(Unit)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - callback(Result.failure(Exception("no logined $errorMessage"))) - } - }) - - return - } - - - if (currentContext == null) { - callback(Result.failure(Exception("Activity not available"))) - return - } - - Log.d(TAG, "开始新的登录流程") - loginManager = QQMusicLoginManager(currentContext) - loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback { - override fun onSuccess() { - handler.post { - ensureMusicManager(currentContext,object: QQMusicManager. BindCallback{ - override fun onSuccess() { - callback(Result.success(Unit)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - callback(Result.failure(Exception("no logined $errorMessage"))) - } - }) - } - } - - override fun onError(errorCode: String, errorMessage: String) { - handler.post { - Log.e(TAG, "登录失败: $errorCode - $errorMessage") - callback(Result.failure(Exception("$errorCode: $errorMessage"))) - } - } - }) - } - - /** - * 登录接口 - */ - fun autologin(callback: (Result) -> Unit) { - if (!isQQMusicInstalled()){ - callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) - return - } - val currentActivity = context - - // 先检查是否已经登录且未过期 - if (isLoggedIn()) { - Log.d(TAG, "用户已登录且未过期,直接返回登录信息") - ensureMusicManager(currentActivity!!,object: QQMusicManager. BindCallback{ - override fun onSuccess() { - callback(Result.success(currentLoginResult!!)) - } - - override fun onError(errorCode: Int, errorMessage: String) { - callback(Result.failure(Exception("no logined $errorMessage"))) - } - }) - - return - }else{ - callback(Result.failure(Exception("no logined"))) - return - } - } - - /** - * 登出 - */ - fun logout() { - currentLoginResult = null - clearSavedLoginInfo() - Log.d(TAG, "用户已登出,清除本地登录信息") - } - - /** - * 搜索歌曲(带自动登录检查和重试机制) - */ - fun searchSongs( - keyword: String, - searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, - callback: (Result>>) -> Unit - ) { - executeSearchbackAction("搜索歌曲",0,2) { musicManager, searchCallback -> - musicManager.searchSongs(keyword,searchType,searchCallback) - }(callback) - } - - - /** - * 播放歌曲列表(带自动登录检查) - */ - fun playSongs( - songMids: List, - index: Int = 0, - callback: (Result) -> Unit - ) { - executePlaybackAction("播放列表",0,2) { musicManager, playCallback -> - Log.d(TAG, "播放歌曲:${songMids[index]}") - musicManager.playSongs(songMids, index,playCallback) - }(callback) - } - - /** - * 播放音乐 - */ - fun playMusic(callback: (Result) -> Unit) { - executePlaybackAction("播放",0,2) { musicManager, playCallback -> - musicManager.playMusic(playCallback) - }(callback) - } - - /** - * 暂停音乐 - */ - fun pauseMusic(callback: (Result) -> Unit) { - if (context == null || musicManager == null) { - callback(Result.failure(Exception("服务未初始化"))) - return - } - musicManager?.pauseMusic(object : QQMusicManager.PlayCallback { - override fun onSuccess() { - handler.post { - Log.d(TAG, "暂停成功") - callback(Result.success(Unit)) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - handler.post { - Log.e(TAG, "暂停失败: $errorMessage") - callback(Result.failure(Exception("暂停失败: $errorMessage"))) - } - } - }) - } - - - /** - * 暂停音乐 - */ - fun autopauseMusic(callback: (Result) -> Unit) { - if (context == null || musicManager == null) { - callback(Result.failure(Exception("服务未初始化"))) - return - } - musicManager?.autopauseMusic(object : QQMusicManager.PlayCallback { - override fun onSuccess() { - handler.post { - Log.d(TAG, "暂停成功") - callback(Result.success(Unit)) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - handler.post { - Log.e(TAG, "暂停失败: $errorMessage") - callback(Result.failure(Exception("暂停失败: $errorMessage"))) - } - } - }) - } - - /** - * 恢复播放 - */ - fun resumeMusic(callback: (Result) -> Unit) { - executePlaybackAction("恢复播放",0,0) { musicManager, playCallback -> - musicManager.resumeMusic(playCallback) - }(callback) - } - - /** - * 停止音乐 - */ - fun stopMusic(callback: (Result) -> Unit) { - executePlaybackAction("停止",0,0) { musicManager, playCallback -> - musicManager.stopMusic(playCallback) - }(callback) - } - - /** - * 下一首 - */ - fun skipToNext(callback: (Result) -> Unit) { - executePlaybackAction("下一首",0,0) { musicManager, playCallback -> - musicManager.skipToNext(playCallback) - }(callback) - } - - /** - * 上一首 - */ - fun skipToPrevious(callback: (Result) -> Unit) { - executePlaybackAction("上一首",0,0) { musicManager, playCallback -> - musicManager.skipToPrevious(playCallback) - }(callback) - } - - /** - * 获取播放状态 - */ - fun getPlaybackState(callback: (Result>) -> Unit) { - executeInfoAction("获取播放状态") { musicManager, infoCallback -> - musicManager.getPlaybackState(infoCallback) - }(callback) - } - - /** - * 获取当前歌曲信息 - */ - fun getCurrentSong(callback: (Result>) -> Unit) { - executeInfoAction("获取当前歌曲") { musicManager, infoCallback -> - musicManager.getCurrentSong(infoCallback) - }(callback) - } - - /** - * 获取播放时间 - */ - fun getPlayTime(callback: (Result>) -> Unit) { - executeInfoAction("获取播放时间") { musicManager, infoCallback -> - musicManager.getPlayTime(infoCallback) - }(callback) - - } - - /** - * 同步当前播放信息 - 统一接口 - */ - fun syncCurrentPlayInfo(callback: (Result>) -> Unit) { - executeInfoAction("同步当前播放信息") { musicManager, infoCallback -> - musicManager.syncCurrentPlayInfo(infoCallback) - }(callback) - } - - /** - * 清理资源 - */ - fun cleanup() { - // 清理搜索超时任务 - searchTimeouts.values.forEach { handler.removeCallbacks(it) } - searchTimeouts.clear() - - // 清理管理器 - loginManager?.cleanup() - loginManager = null - musicManager?.cleanup() - musicManager = null - context = null - Log.d(TAG, "QQ音乐单例资源清理完成") - } - - /** - * 保存登录信息到本地 - */ - private fun saveLoginInfo(loginResult: LoginResult) { - sharedPreferences?.edit()?.apply { - putString(KEY_OPEN_ID, loginResult.openId) - putString(KEY_OPEN_TOKEN, loginResult.openToken) - putString(KEY_EXPIRE_TIME, loginResult.expireTime) - apply() - } - Log.d(TAG, "登录信息已保存到本地") - } - - /** - * 从本地加载登录信息 - */ - private fun loadSavedLoginInfo() { - val prefs = sharedPreferences ?: return - - val openId = prefs.getString(KEY_OPEN_ID, null) - val openToken = prefs.getString(KEY_OPEN_TOKEN, null) - val expireTime = prefs.getString(KEY_EXPIRE_TIME, null) - - if (openId != null && openToken != null && expireTime != null) { - currentLoginResult = LoginResult(openId, openToken, expireTime) - Log.d(TAG, "从本地加载登录信息成功") - } else { - Log.d(TAG, "本地无保存的登录信息") - } - } - - /** - * 清除本地保存的登录信息 - */ - private fun clearSavedLoginInfo() { - sharedPreferences?.edit()?.apply { - remove(KEY_OPEN_ID) - remove(KEY_OPEN_TOKEN) - remove(KEY_EXPIRE_TIME) - apply() - } - Log.d(TAG, "本地登录信息已清除") - } - - // 私有辅助方法 - private fun ensureMusicManager(context: Context,callback: QQMusicManager. BindCallback) { - if (musicManager == null) { - musicManager = QQMusicManager(context) - musicManager!!.bindQQMusicApiService(callback) - }else{ - if(!musicManager!!.isConnected) { //未连接 - musicManager?.bindQQMusicApiService(callback) - }else{ - callback.onSuccess() - } - } - } - - - private fun executeSearchbackAction( - actionName: String, - curragain:Int, - allagain:Int, - action: (QQMusicManager, QQMusicManager.SearchCallback) -> Unit - ): (callback: (Result>>) -> Unit) -> Unit { - return lambda@{ callback -> - - if (!isQQMusicInstalled()){ - callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) - return@lambda - } - - val currentActivity = context - if (currentActivity == null) { - callback(Result.failure(Exception("Activity not available"))) - return@lambda - } - - // 添加登录状态检查 - if (!isLoggedIn()) { - Log.d(TAG, "用户未登录,开始自动登录") - login { loginResult -> - if (loginResult.isSuccess) { - Log.d(TAG, "自动登录成功,继续执行 ${actionName}") - // 登录成功后继续执行原操作 - executeSearchAction(actionName, curragain, allagain, action, callback) - } else { - Log.e(TAG, "自动登录失败: ${loginResult.exceptionOrNull()?.message}") - callback(Result.failure(Exception("用户未登录且自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) - } - } - return@lambda - } - - // 用户已登录,直接执行搜索操作 - executeSearchAction(actionName, curragain, allagain, action, callback) - } - } - - // 提取搜索执行逻辑为独立方法 - private fun executeSearchAction( - actionName: String, - curragain: Int, - allagain: Int, - action: (QQMusicManager, QQMusicManager.SearchCallback) -> Unit, - callback: (Result>>) -> Unit - ) { - Log.d(TAG, "开始 ${actionName}") - ensureMusicManager(context!!, object: QQMusicManager.BindCallback{ - override fun onSuccess(){ - action(musicManager!!, object : QQMusicManager.SearchCallback { - override fun onSuccess(songs: List>) { - Log.d(TAG, "${actionName} 成功") - handler.post { - callback(Result.success(songs)) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - Log.d(TAG, "失败 ${actionName} errorCode: $errorCode errorMessage:$errorMessage 当前重试${curragain}/${allagain}") - if (allagain > 0 && curragain < allagain){ //重试机制 - Log.d(TAG, "重试 ${actionName} 当前重试${curragain+1}/${allagain}") - executeSearchbackAction(actionName,curragain+1,allagain,action)(callback) - }else{ - handler.post { - callback(Result.failure(Exception("$actionName 失败: $errorMessage"))) - } - } - } - }) - } - override fun onError(errorCode: Int, errorMessage: String) { - Log.d(TAG, "失败 ${actionName} errorCode: $errorCode errorMessage:$errorMessage 当前重试${curragain}/${allagain}") - if (allagain > 0 && curragain < allagain){ //重试机制 - Log.d(TAG, "重试 ${actionName} 当前重试${curragain+1}/${allagain}") - executeSearchbackAction(actionName,curragain+1,allagain,action)(callback) - }else{ - handler.post { - callback(Result.failure(Exception("$actionName 失败: $errorMessage"))) - } - } - } - }) - } - - - - private fun executePlaybackAction( - actionName: String, - curragain:Int, - allagain:Int, - action: (QQMusicManager, QQMusicManager.PlayCallback) -> Unit - ): (callback: (Result) -> Unit) -> Unit { - return lambda@{ callback -> - - if (!isQQMusicInstalled()){ - callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) - return@lambda - } - - val currentActivity = context - if (currentActivity == null) { - callback(Result.failure(Exception("Activity not available"))) - return@lambda - } - - // 添加登录状态检查 - if (!isLoggedIn()) { - Log.d(TAG, "用户未登录,开始自动登录") - login { loginResult -> - if (loginResult.isSuccess) { - Log.d(TAG, "自动登录成功,继续执行 ${actionName}") - // 登录成功后继续执行原操作 - executePlayAction(actionName, curragain, allagain, action, callback) - } else { - Log.e(TAG, "自动登录失败: ${loginResult.exceptionOrNull()?.message}") - callback(Result.failure(Exception("用户未登录且自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) - } - } - return@lambda - } - - // 用户已登录,直接执行搜索操作 - executePlayAction(actionName, curragain, allagain, action, callback) - - } - } - - private fun executePlayAction( - actionName: String, - curragain:Int, - allagain:Int, - action: (QQMusicManager, QQMusicManager.PlayCallback) -> Unit, - callback: (Result) -> Unit - ) { - Log.d(TAG, "开始 ${actionName}") - ensureMusicManager(context!!,object: QQMusicManager. BindCallback{ - override fun onSuccess(){ - action(musicManager!!, object : QQMusicManager.PlayCallback { - override fun onSuccess() { - Log.d(TAG, "${actionName}成功") - handler.post { - callback(Result.success(Unit)) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - if (allagain > 0 && curragain < allagain){ //重试机制 - executePlaybackAction(actionName,curragain+1,allagain,action)(callback) - }else{ - //播放失败时,检查当前播放状态 - Log.d(TAG, "${actionName}失败,检查当前播放状态: $errorCode $errorMessage") - checkPlaybackStateOnError(actionName, errorCode, errorMessage, callback) - } - } - }) - } - override fun onError(errorCode: Int, errorMessage: String) { - Log.e(TAG, "${actionName}失败: $errorCode $errorMessage") - if (allagain > 0 && curragain < allagain){ //重试机制 - executePlaybackAction(actionName,curragain+1,allagain,action)(callback) - }else{ - callback(Result.failure(Exception("$actionName 失败: $errorMessage"))) - } - } - }) - } - - /** - * 播放失败时检查播放状态 - */ - private fun checkPlaybackStateOnError( - actionName: String, - errorCode: Int, - errorMessage: String, - callback: (Result) -> Unit - ) { - musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback { - override fun onSuccess(data: Map) { - val playState = data["state"] as? Int ?: 0 - val isPlaying = when (playState) { - PlayState.STARTED -> true - PlayState.PREPARED -> true - PlayState.BUFFERING -> true - PlayState.PAUSED -> false - PlayState.STOPPED -> false - PlayState.ERROR -> false - else -> false - } - - handler.post { - if (isPlaying) { - Log.d(TAG, "${actionName}虽然返回失败,但检查到正在播放,返回成功") - callback(Result.success(Unit)) - } else { - Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage") - if (errorCode == -1){ - musicManager?.bindQQMusicApiService() - } - callback(Result.failure(Exception("播放失败: $errorMessage"))) - } - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - // 获取播放状态失败,直接返回原始错误 - handler.post { - Log.e(TAG, "${actionName}失败且未在播放: $errorCode $errorMessage") - if (errorCode == -1){ - musicManager?.bindQQMusicApiService() - } - callback(Result.failure(Exception("播放失败: $errorMessage"))) - } - } - }) - } - - - - private fun executeInfoAction( - actionName: String, - action: (QQMusicManager, QQMusicManager.InfoCallback) -> Unit - ): (callback: (Result>) -> Unit) -> Unit { - return lambda@{ callback -> - if (!isQQMusicInstalled()){ - callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) - return@lambda - } - val currentcontext = context - if (currentcontext == null) { - callback(Result.failure(Exception("Activity not available"))) - return@lambda - } - - ensureMusicManager(currentcontext,object: QQMusicManager. BindCallback{ - override fun onSuccess(){ - action(musicManager!!, object : QQMusicManager.InfoCallback { - override fun onSuccess(data: Map) { - handler.post { - Log.d(TAG, "${actionName}成功") - callback(Result.success(data)) - } - } - - override fun onError(errorCode: Int, errorMessage: String) { - handler.post { - Log.e(TAG, "${actionName}失败: $errorMessage") - callback(Result.failure(Exception("${actionName}失败: $errorMessage"))) - } - } - }) - } - override fun onError(errorCode: Int, errorMessage: String) { - callback(Result.failure(Exception("$actionName 失败: $errorMessage"))) - } - }) - } - } - - // 数据类定义 - data class LoginResult( - val openId: String, - val openToken: String, - val expireTime: String - ) - - data class PlayTimeInfo( - val totalTime: Long, - val currentTime: Long - ) -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt deleted file mode 100644 index d1367ba2..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt +++ /dev/null @@ -1,237 +0,0 @@ -package com.yunqiinnovation.qq_music - -import android.app.Activity -import android.util.Log -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import com.tencent.qqmusic.third.api.contract.* - -/** QqMusicPlugin */ -class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { - private lateinit var channel: MethodChannel -// private var activity: Activity? = null - private val qqMusicSingleton = QQMusicSingleton.getInstance() - private var isRegistered = false - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - if (isRegistered) { - Log.w("QqMusicPlugin", "Plugin already registered, skipping") - return - } - - Log.d("QqMusicPlugin", "onAttachedToEngine called") - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music") - channel.setMethodCallHandler(this) - isRegistered = true - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - Log.d("QqMusicPlugin", "onDetachedFromEngine called") - if (::channel.isInitialized) { - channel.setMethodCallHandler(null) - } - isRegistered = false - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - Log.d("QqMusicPlugin", "onAttachedToActivity called") -// activity = binding.activity - println("QQ音乐 onAttachedToActivity") -// qqMusicSingleton.initialize(activity!!) - Log.d("QqMusicPlugin", "qqMusicSingleton.initialize completed") - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - Log.d("QqMusicPlugin", "onReattachedToActivityForConfigChanges called") -// activity = binding.activity - // qqMusicSingleton.initialize(activity!!) - } - - override fun onDetachedFromActivityForConfigChanges() { - Log.d("QqMusicPlugin", "onDetachedFromActivityForConfigChanges called") -// activity = null - } - - override fun onDetachedFromActivity() { - Log.d("QqMusicPlugin", "onDetachedFromActivity called") -// activity = null -// qqMusicSingleton.cleanup() - } - - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "login" -> { - qqMusicSingleton.autologin { kotlinResult -> - kotlinResult.fold( - onSuccess = { loginResult -> - result.success(mapOf( - "success" to true, - "openId" to loginResult.openId, - "openToken" to loginResult.openToken, - "expireTime" to loginResult.expireTime - )) - }, - onFailure = { error -> - result.error("LOGIN_FAILED", error.message, null) - } - ) - } - } - "searchSongs" -> { - val keyword = call.argument("keyword") - val searchType = call.argument("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG - - if (keyword == null) { - result.error("INVALID_ARGUMENT", "keyword is required", null) - return - } - - qqMusicSingleton.searchSongs(keyword, searchType) { kotlinResult -> - kotlinResult.fold( - onSuccess = { songs -> - result.success(mapOf( - "success" to true, - "songs" to songs - )) - }, - onFailure = { error -> - result.error("SEARCH_FAILED", error.message, null) - } - ) - } - } - "playSongs" -> { - val songMids = call.argument>("songMids") - val index = call.argument("index") ?: 0 - - if (songMids == null || songMids.isEmpty()) { - result.error("INVALID_ARGUMENT", "songMids is required", null) - return - } - - qqMusicSingleton.playSongs(songMids, index) { kotlinResult -> - kotlinResult.fold( - onSuccess = { - result.success(mapOf("success" to true)) - }, - onFailure = { error -> - result.error("PLAY_FAILED", error.message, null) - } - ) - } - } - "playMusic" -> { - qqMusicSingleton.playMusic { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("PLAY_FAILED", error.message, null) } - ) - } - } - "pauseMusic" -> { - qqMusicSingleton.pauseMusic { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("PAUSE_FAILED", error.message, null) } - ) - } - } - "autopauseMusic" -> { - qqMusicSingleton.autopauseMusic { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("PAUSE_FAILED", error.message, null) } - ) - } - } - "resumeMusic" -> { - qqMusicSingleton.resumeMusic { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("RESUME_FAILED", error.message, null) } - ) - } - } - "stopMusic" -> { - qqMusicSingleton.stopMusic { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("STOP_FAILED", error.message, null) } - ) - } - } - "skipToNext" -> { - qqMusicSingleton.skipToNext { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("SKIP_FAILED", error.message, null) } - ) - } - } - "skipToPrevious" -> { - qqMusicSingleton.skipToPrevious { kotlinResult -> - kotlinResult.fold( - onSuccess = { result.success(mapOf("success" to true)) }, - onFailure = { error -> result.error("SKIP_FAILED", error.message, null) } - ) - } - } - "getPlaybackState" -> { - qqMusicSingleton.getPlaybackState { kotlinResult -> - kotlinResult.fold( - onSuccess = { data -> - result.success(mapOf( - "success" to true, - "state" to data["state"] - )) - }, - onFailure = { error -> - result.error("GET_STATE_FAILED", error.message, null) - } - ) - } - } - "getCurrentSong" -> { - qqMusicSingleton.getCurrentSong { kotlinResult -> - kotlinResult.fold( - onSuccess = { data -> - result.success(mapOf( - "success" to true, - "song" to data - )) - }, - onFailure = { error -> - result.error("GET_SONG_FAILED", error.message, null) - } - ) - } - } - "syncCurrentPlayInfo" -> { - qqMusicSingleton.syncCurrentPlayInfo { kotlinResult -> - kotlinResult.fold( - onSuccess = { data -> - result.success(mapOf( - "success" to true, - "data" to data - )) - }, - onFailure = { error -> - result.error("SYNC_FAILED", error.message, null) - } - ) - } - } - "getPlatformVersion" -> { - result.success("Android ${android.os.Build.VERSION.RELEASE}") - } - else -> { - result.notImplemented() - } - } - } -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java deleted file mode 100644 index d60641ea..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.yunqiinnovation.qq_music.openid; - -import android.util.Base64; - -import com.yunqiinnovation.qq_music.Config; -import com.tencent.qqmusic.third.api.contract.Keys; - -import org.json.JSONObject; - -public class OpenIDHelper { - - private static final String QQMusicPublicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrp4sMcJjY9hb2J3sHWlwIEBrJlw2Cimv+rZAQmR8V3EI+0PUK14pL8OcG7CY79li30IHwYGWwUapADKA01nKgNeq7+rSciMYZv6ByVq+ocxKY8az78HwIppwxKWpQ+ziqYavvfE5+iHIzAc8RvGj9lL6xx1zhoPkdaA0agAyuMQIDAQAB"; - - - public static String getEncryptString(String nonce) { - if (nonce == null || nonce.isEmpty()) { - return null; - } - - try { - //1.使用App私钥签名 - 修改这里,使用Base64格式的私钥 - String signString = RSAUtils.sign(nonce.getBytes(), Config.getBase64PrivateKey()); - - JSONObject signJson = new JSONObject(); - signJson.put(Keys.API_RETURN_KEY_NONCE, nonce); - signJson.put(Keys.API_RETURN_KEY_SIGN, signString); - signJson.put(Keys.API_RETURN_KEY_CALLBACK_URL, "eaimar://"); - String sourceString = signJson.toString(); - - //2. 使用Q音公钥加密(随机数+签名) - byte[] encryptData = RSAUtils.encryptByPublicKey(sourceString.getBytes(), QQMusicPublicKey); - if (encryptData == null) { - return null; - } - return Base64.encodeToString(encryptData, Base64.DEFAULT); - } catch (Exception e) { - e.printStackTrace(); - return null; - } - } - - public static String decryptQQMEncryptString(String qmEncryptString) { - try { - byte[] qmEncryptData = Base64.decode(qmEncryptString, Base64.DEFAULT); - //7.使用App私钥解密 - 修改这里,使用Base64格式的私钥 - byte[] decryptData = RSAUtils.decryptByPrivateKey(qmEncryptData, Config.getBase64PrivateKey()); - if (decryptData != null) { - return new String(decryptData); - } - } catch (Exception e) { - e.printStackTrace(); - } - return null; - } - - public static boolean checkQMSign(String sign, String nonce) { - try { - return RSAUtils.verify(nonce.getBytes(), QQMusicPublicKey, sign); - } catch (Exception e) { - e.printStackTrace(); - } - return false; - } -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java deleted file mode 100644 index 0a0704fe..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java +++ /dev/null @@ -1,314 +0,0 @@ -package com.yunqiinnovation.qq_music.openid; - -import android.util.Base64; - -import java.io.ByteArrayOutputStream; -import java.security.Key; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.Signature; -import java.security.interfaces.RSAPrivateKey; -import java.security.interfaces.RSAPublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.HashMap; -import java.util.Map; - -import javax.crypto.Cipher; - -public class RSAUtils { - /** - * 加密算法RSA - */ - public static final String KEY_ALGORITHM = "RSA"; - - /** - * 签名算法 - */ - public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";//"MD5withRSA"; - - /** - * 获取公钥的key - */ - private static final String PUBLIC_KEY = "RSAPublicKey"; - - /** - * 获取私钥的key - */ - private static final String PRIVATE_KEY = "RSAPrivateKey"; - - private static final String CIHPER_KEY = "RSA/ECB/PKCS1Padding";// - - /** - * RSA最大加密明文大小 - */ - private static final int MAX_ENCRYPT_BLOCK = 117; - - /** - * RSA最大解密密文大小 - */ - private static final int MAX_DECRYPT_BLOCK = 128; - - /** - * 生成密钥对(公钥和私钥) - * - * @return - * @throws Exception - */ - public static Map genKeyPair() throws Exception { - KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); - keyPairGen.initialize(1024); - KeyPair keyPair = keyPairGen.generateKeyPair(); - RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); - RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); - Map keyMap = new HashMap<>(2); - keyMap.put(PUBLIC_KEY, publicKey); - keyMap.put(PRIVATE_KEY, privateKey); - return keyMap; - } - - /** - * 用私钥对信息生成数字签名 - * - * @param data 已加密数据 - * @param privateKey 私钥(BASE64编码) - * @return - * @throws Exception - */ - public static String sign(byte[] data, String privateKey) throws Exception { - - byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec); - Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); - signature.initSign(privateK); - signature.update(data); - return Base64.encodeToString(signature.sign(), Base64.DEFAULT); - } - - /** - * 校验数字签名 - * - * @param data 已加密数据 - * @param publicKey 公钥(BASE64编码) - * @param sign 数字签名 - * @return - * @throws Exception - */ - public static boolean verify(byte[] data, String publicKey, String sign) - throws Exception { - byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); - X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - PublicKey publicK = keyFactory.generatePublic(keySpec); - Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); - signature.initVerify(publicK); - signature.update(data); - return signature.verify(Base64.decode(sign, Base64.DEFAULT)); - } - - /** - * 私钥解密 - * - * @param encryptedData 已加密数据 - * @param privateKey 私钥(BASE64编码) - * @return - * @throws Exception - */ - public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) - throws Exception { - byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - Key privateK = keyFactory.generatePrivate(pkcs8KeySpec); -// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - Cipher cipher = Cipher.getInstance(CIHPER_KEY); - - cipher.init(Cipher.DECRYPT_MODE, privateK); - int inputLen = encryptedData.length; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段解密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_DECRYPT_BLOCK) { - cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); - } else { - cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_DECRYPT_BLOCK; - } - byte[] decryptedData = out.toByteArray(); - out.close(); - return decryptedData; - } - - public static String testDecryptByJavaPrivKey(String privkey_from_java, String encryptedString) throws Exception { - //加解密类 - Cipher cipher = Cipher.getInstance("RSA");//Cipher.getInstance("RSA/ECB/PKCS1Padding"); - // G0语言通过java产生的公钥加密得到的加密后的字符串 - byte[] enBytes = Base64.decode(encryptedString, Base64.DEFAULT); - //通过密钥字符串得到密钥 - - PrivateKey privateKey = testGetPrivateKey(privkey_from_java); - - - //解密 - cipher.init(Cipher.DECRYPT_MODE, privateKey); - byte[] deBytes = cipher.doFinal(enBytes); - return new String(deBytes); - - } - - public static PrivateKey testGetPrivateKey(String key) throws Exception { - byte[] keyBytes; - keyBytes = Base64.decode(key, Base64.DEFAULT); - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance("RSA"); - return keyFactory.generatePrivate(keySpec); - } - - /** - * 公钥解密 - * - * @param encryptedData 已加密数据 - * @param publicKey 公钥(BASE64编码) - * @return - * @throws Exception - */ - public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) - throws Exception { - byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - Key publicK = keyFactory.generatePublic(x509KeySpec); -// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - Cipher cipher = Cipher.getInstance(CIHPER_KEY); - cipher.init(Cipher.DECRYPT_MODE, publicK); - int inputLen = encryptedData.length; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段解密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_DECRYPT_BLOCK) { - cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); - } else { - cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_DECRYPT_BLOCK; - } - byte[] decryptedData = out.toByteArray(); - out.close(); - return decryptedData; - } - - /** - * 公钥加密 - * - * @param data 源数据 - * @param publicKey 公钥(BASE64编码) - * @return - * @throws Exception - */ - public static byte[] encryptByPublicKey(byte[] data, String publicKey) - throws Exception { - byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); - X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - Key publicK = keyFactory.generatePublic(x509KeySpec); - // 对数据加密 -// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - Cipher cipher = Cipher.getInstance(CIHPER_KEY); - cipher.init(Cipher.ENCRYPT_MODE, publicK); - int inputLen = data.length; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段加密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { - cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK); - } else { - cache = cipher.doFinal(data, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_ENCRYPT_BLOCK; - } - byte[] encryptedData = out.toByteArray(); - out.close(); - return encryptedData; - } - - /** - * 私钥加密 - * - * @param data 源数据 - * @param privateKey 私钥(BASE64编码) - * @return - * @throws Exception - */ - public static byte[] encryptByPrivateKey(byte[] data, String privateKey) - throws Exception { - byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); - PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); - KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); - Key privateK = keyFactory.generatePrivate(pkcs8KeySpec); -// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); - Cipher cipher = Cipher.getInstance(CIHPER_KEY); - cipher.init(Cipher.ENCRYPT_MODE, privateK); - int inputLen = data.length; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - int offSet = 0; - byte[] cache; - int i = 0; - // 对数据分段加密 - while (inputLen - offSet > 0) { - if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { - cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK); - } else { - cache = cipher.doFinal(data, offSet, inputLen - offSet); - } - out.write(cache, 0, cache.length); - i++; - offSet = i * MAX_ENCRYPT_BLOCK; - } - byte[] encryptedData = out.toByteArray(); - out.close(); - return encryptedData; - } - - /** - * 获取私钥 - * - * @param keyMap 密钥对 - * @return - */ - public static String getPrivateKey(Map keyMap) { - Key key = (Key) keyMap.get(PRIVATE_KEY); - return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); - } - - /** - * 获取公钥 - * - * @param keyMap 密钥对 - * @return - */ - public static String getPublicKey(Map keyMap) { - Key key = (Key) keyMap.get(PUBLIC_KEY); - return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); - } -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt deleted file mode 100644 index 7a8a868e..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt +++ /dev/null @@ -1,253 +0,0 @@ -package com.yunqiinnovation.qq_music.pcm - -import android.media.AudioFormat -import android.media.AudioManager -import android.media.AudioRecord -import android.media.AudioTrack -import android.media.AudioTrack.* -import android.os.Build -import android.os.ParcelFileDescriptor -import android.os.Process -import android.util.Log -import com.yunqiinnovation.qq_music.util.BitConverter -import java.io.DataInputStream -import java.io.File -import java.io.FileInputStream -import java.io.InputStream -import java.util.concurrent.ConcurrentLinkedQueue - - -class AudioTrackManager { - companion object { - private const val TAG = "AudioTrackManager" - private const val LOG_LOOP = true - - //音频流类型 - private const val mStreamType = AudioManager.STREAM_MUSIC - - //指定采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置) - private const val mSampleRateInHz = 44100 - - //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量 - private const val mChannelConfig = AudioFormat.CHANNEL_IN_STEREO //STEREO立体声 - - //指定音频量化位数 ,在AudioFormaat类中指定了以下各种可能的常量。通常我们选择ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。 - //因此可以设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实。 - private const val mAudioFormat = AudioFormat.ENCODING_PCM_FLOAT - - //STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样, - // 应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。 - private const val mMode = AudioTrack.MODE_STREAM - } - - private var mAudioTrack: AudioTrack? = null - - private var mInputStream: InputStream? = null //播放的数据流 - private var mPlayerThread: Thread? = null - - //指定缓冲区大小。调用AudioRecord类的getMinBufferSize方法可以获得。 - private var mMinBufferSize = 0 - - var printMessageCallback: ((String) -> Unit)? = null - var enterForeground: ((Boolean) -> Unit)? = null - - var isFloatPcm = true - - fun initData(sampleRateInHz: Int = mSampleRateInHz, channelConfig: Int = mChannelConfig, audioFormat: Int = mAudioFormat) { - Log.d(TAG, "[initData】$sampleRateInHz, $channelConfig, $audioFormat") - //根据采样率,采样精度,单双声道来得到frame的大小。 - mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) //计算最小缓冲区 - //注意,按照数字音频的知识,这个算出来的是一秒钟buffer的大小。 - //创建AudioTrack - mAudioTrack = AudioTrack(mStreamType, sampleRateInHz, channelConfig, audioFormat, mMinBufferSize, mMode) - isFloatPcm = audioFormat == AudioFormat.ENCODING_PCM_FLOAT - play() - } - - /** - * 销毁线程方法 - */ - private fun destroyThread() { - try { - if (null != mPlayerThread && Thread.State.RUNNABLE == mPlayerThread!!.state) { - try { - Thread.sleep(500) - mPlayerThread!!.interrupt() - } catch (e: Exception) { - mPlayerThread = null - } - } - mPlayerThread = null - } catch (e: Exception) { - e.printStackTrace() - } finally { - mPlayerThread = null - } - } - - /** - * 启动播放线程 - */ - private fun startThread() { - destroyThread() - if (mPlayerThread == null) { - mPlayerThread = Thread(streamPlayerRunnable) - mPlayerThread!!.start() - } - } - - private val streamPlayerRunnable = Runnable { - try { - //设置线程的优先级 - Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) - Log.d(TAG, "streamPlayerRunnable") - - val tempBuffer = ByteArray(mMinBufferSize * 4) - var readCount = 0 - while (true) { - if (LOG_LOOP) { - //Log.d(TAG, "while true") - } - if (mInputStream!!.available() <= 0) { - if (LOG_LOOP) { - //Log.d(TAG, "sleep") - } - printMessageCallback?.invoke("Play thread sleep \n PlayState:${mAudioTrack?.playState?.playStateToString()}") - Thread.sleep(500) - continue - } - - readCount = mInputStream!!.read(tempBuffer) - if (LOG_LOOP) { - Log.d(TAG, "readCount:$readCount") - } - if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) { - continue - } - if (readCount != 0 && readCount != -1) { - //判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED - if (mAudioTrack!!.state == AudioTrack.STATE_UNINITIALIZED) { - initData() - } - - printMessageCallback?.invoke("AudioTrack write $readCount Bytes\n PlayState:${mAudioTrack?.playState?.playStateToString()}") - - if (isFloatPcm) { - val size = readCount / 4 - val audioData = FloatArray(size) - for (i in 0 until size) { - // 框架地址:https://github.com/smartdevicelink/sdl_java_suite/blob/master/base/src/main/java/com/smartdevicelink/util/BitConverter.java - val bitInt = BitConverter.intFromByteArray(tempBuffer, i * 4) - val fl = java.lang.Float.intBitsToFloat(bitInt) - audioData[i] = fl - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - val ret = mAudioTrack?.write(audioData, 0, audioData.size, WRITE_BLOCKING) - Log.d(TAG, "ret=$ret") - } - } else { - mAudioTrack!!.write(tempBuffer, 0, readCount) - } - - } - } - } catch (e: Exception) { - e.printStackTrace() - } - } - - fun Int.playStateToString(): String { - return when (this) { - PLAYSTATE_STOPPED -> { - "PLAYSTATE_STOPPED" - } - PLAYSTATE_PAUSED -> { - "PLAYSTATE_PAUSED" - } - PLAYSTATE_PLAYING -> { - "PLAYSTATE_PLAYING" - } - else -> { - "unknown" - } - } - } - - /** - * 播放文件 - * - * @param path - * @throws Exception - */ - @Throws(Exception::class) - private fun setPath(path: String) { - val file = File(path) - mInputStream = DataInputStream(FileInputStream(file)) - } - - /** - * 启动播放 - * - * @param path - */ - fun startPlay(path: String) { - try { - //AudioTrack未初始化 - setPath(path) - startThread() - } catch (e: Exception) { - Log.d(TAG, "startPlay: Exception$e") - } - } - - fun startPlay(inputStream: InputStream?) { - initData() - try { - mInputStream = DataInputStream(inputStream) - startThread() - } catch (e: Exception) { - Log.d(TAG, "startPlay: Exception$e") - } - } - - fun startPlayByFileDescriptor(fileDescriptor: ParcelFileDescriptor) { - mInputStream = ParcelFileDescriptor.AutoCloseInputStream(fileDescriptor) - startThread() - } - - /** - * 停止播放 - */ - fun stopPlay() { - try { - destroyThread() //销毁线程 - if (mAudioTrack != null) { - if (mAudioTrack!!.state == AudioRecord.STATE_INITIALIZED) { //初始化成功 - mAudioTrack!!.stop() //停止播放 - } - if (mAudioTrack != null) { - mAudioTrack!!.release() //释放audioTrack资源 - } - mAudioTrack = null - } - if (mInputStream != null) { - mInputStream!!.close() //关闭数据输入流 - } - } catch (e: Exception) { - e.printStackTrace() - } - enterForeground?.invoke(false) - } - - - fun pause() { - mAudioTrack?.pause() - enterForeground?.invoke(false) - } - - fun play() { - mAudioTrack?.play() - enterForeground?.invoke(true) - } - -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt deleted file mode 100644 index 557ec479..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt +++ /dev/null @@ -1,403 +0,0 @@ -package com.yunqiinnovation.qq_music.pcm - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.media.AudioFormat -import android.os.* -import androidx.core.app.NotificationCompat -import android.util.Log -import android.widget.Toast -import com.yunqiinnovation.qq_music.* -import com.yunqiinnovation.qq_music.R -import com.yunqiinnovation.qq_music.openid.OpenIDHelper -import com.yunqiinnovation.qq_music.util.QPlayBindHelper -import com.tencent.qqmusic.third.api.contract.* -import org.json.JSONObject - -/** - * Created by clydeazhang on 2021/5/11 6:43 PM. - * Copyright (c) 2021 Tencent. All rights reserved. - */ -class PlayerService : Service() { - - companion object { - private const val TAG = "PlayerService" - private const val KEY_ENTER_FOREGROUND = "KEY_ENTER_FOREGROUND" - private const val CHANNEL_ID = "default" - private const val NATIVE_LOCALIZATION_PREF_NAME = "native_localization" - private const val NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON = "strings_json" - } - - private val qPlayBindHelper = QPlayBindHelper(this) - private var qqMusicApi: IQQMusicApi? = null - private var curPlayState: Int = 0 - private val handler = Handler(Looper.getMainLooper()) - - private var audioManager: AudioTrackManager? = null - private var audioManagerFloat: AudioTrackManager? = null - private var isReceiverRegistered = false - - /** - * QQ音乐事件回调 - */ - private val eventListener = object : IQQMusicApiEventListener.Stub() { - override fun onEvent(event: String, extra: Bundle) { - extra.classLoader = this@PlayerService.classLoader - Log.d(TAG, "onEvent:$event extra:$extra") // 直接使用 Bundle.toString() - handler.post { - when (event) { - Events.API_EVENT_PLAY_STATE_CHANGED -> { - curPlayState = extra.getInt(Keys.API_EVENT_KEY_PLAY_STATE) - } - Events.API_EVENT_MEDIA_INFO_CHANGED -> { - onMediaInfoChanged(extra) - onMediaInfoChangedFloat(extra) - } - } - } - } - } - - private var activeBroadcastReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - val ret = intent?.extras?.get("ret") - if (ret == "0") { - Log.d(TAG, "activeBroadcastReceiver 授权成功") - Toast.makeText(this@PlayerService, "授权成功", Toast.LENGTH_SHORT).show() - qqMusicApi?.registerEventListener( - arrayListOf( - Events.API_EVENT_PLAY_STATE_CHANGED, - Events.API_EVENT_PLAY_SONG_CHANGED, - Events.API_EVENT_MEDIA_INFO_CHANGED - ), eventListener - ) - } else { - Log.d(TAG, "activeBroadcastReceiver 授权失败($ret)") - } - } - } - - override fun onCreate() { - super.onCreate() - try { - val filter = IntentFilter() - filter.addAction("callback_verify_notify") - registerReceiver(activeBroadcastReceiver, filter) - isReceiverRegistered = true - } catch (e: Exception) { - Log.e(TAG, "Failed to register receiver", e) - } - - val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channelName = "默认" - val importance = NotificationManager.IMPORTANCE_DEFAULT - val channel = NotificationChannel(CHANNEL_ID, channelName, importance) - notificationManager.createNotificationChannel(channel) - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val enterForeground = intent?.getBooleanExtra(KEY_ENTER_FOREGROUND, false) ?: false - Log.d(TAG, "onStartCommand, enter foreground=$enterForeground") - if (enterForeground) { - val title = getNativeLocalizedString("native.notification.appName", "EAIMAR") - val content = getNativeLocalizedString( - "native.notification.qqMusic.playing", - "Playing music" - ) - val notification = NotificationCompat.Builder(this, CHANNEL_ID) - .setSmallIcon(R.drawable.ic_launcher_background) - .setContentTitle(title) - .setContentText(content) - .build() - startForeground(1, notification) - } - return super.onStartCommand(intent, flags, startId) - } - - private fun getNativeLocalizedString(key: String, defaultValue: String): String { - return try { - val prefs = getSharedPreferences(NATIVE_LOCALIZATION_PREF_NAME, Context.MODE_PRIVATE) - val json = prefs.getString(NATIVE_LOCALIZATION_PREF_KEY_STRINGS_JSON, null) ?: return defaultValue - val obj = JSONObject(json) - val value = obj.optString(key, defaultValue) - if (value.isBlank()) defaultValue else value - } catch (_: Exception) { - defaultValue - } - } - - override fun onDestroy() { - super.onDestroy() - try { - if (isReceiverRegistered) { - unregisterReceiver(activeBroadcastReceiver) - isReceiverRegistered = false - } - } catch (e: Exception) { - Log.e(TAG, "Failed to unregister receiver", e) - } - - audioManager?.stopPlay() - audioManagerFloat?.stopPlay() - audioManager?.enterForeground = null - audioManagerFloat?.enterForeground = null - } - - private var printer: IPrint? = null - override fun onBind(intent: Intent?): IBinder? { - return object : IPlayService.Stub() { - override fun bindService() { - this@PlayerService.bindService() - } - - override fun startPcmMode() { - this@PlayerService.startPcmMode() - } - - override fun stopPcmMode() { - this@PlayerService.stopPcmMode() - } - - override fun resumeOrPause() { - this@PlayerService.resumeOrPause() - } - - override fun playNext() { - this@PlayerService.playNext() - } - - override fun setPrintMessageCallback(callback: IPrint?) { - this@PlayerService.printer = callback - } - } - } - - private fun playNext() { - val result = qqMusicApi?.execute("skipToNext", null) - val errorCode = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 - if (errorCode != ErrorCodes.ERROR_OK) { - Log.d(TAG, "下一首失败($errorCode)") - } - } - - private fun resumeOrPause() { - if (isPlaying()) { - Log.d(TAG, "pauseMusic") - val result = qqMusicApi?.execute("pauseMusic", null) - val errorCode = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 - if (errorCode != ErrorCodes.ERROR_OK) { - Log.d(TAG, "暂停音乐失败($errorCode)") - } - audioManager?.pause() - audioManagerFloat?.pause() - } else { - if (curPlayState == PlayState.PAUSED) { - Log.d(TAG, "resumeMusic") - val result = qqMusicApi?.execute("resumeMusic", null) - val errorCode = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 - if (errorCode != ErrorCodes.ERROR_OK) { - Log.d(TAG, "继续播放音乐失败($errorCode)") - } - } else { - Log.d(TAG, "playMusic") - val result = qqMusicApi?.execute("playMusic", null) - val errorCode = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 - if (errorCode != ErrorCodes.ERROR_OK) { - Log.d(TAG, "开始播放音乐失败($errorCode)") - } - } - audioManager?.play() - audioManagerFloat?.play() - } - } - - private fun stopPcmMode() { - audioManager?.stopPlay() - audioManagerFloat?.stopPlay() - qqMusicApi?.execute("stopPcmMode", null) - } - - private fun startPcmMode() { - Log.d(TAG, "startPcmMode called") - - qqMusicApi?.executeAsync("startPcmMode", null, object : IQQMusicApiCallback.Stub() { - override fun onReturn(registerResult: Bundle?) { - registerResult?.classLoader = classLoader - - val ret = registerResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 - Log.d(TAG, "onReturn $ret") - if (ret != ErrorCodes.ERROR_OK) { - Log.d(TAG, "startPcmMode return failed, ret=$ret") - return - } - } - }) - } - - private fun onMediaInfoChanged(registerResult: Bundle?) { - audioManager?.stopPlay() - audioManager = AudioTrackManager() - - audioManager?.printMessageCallback = { - printer?.print(it) - } - - audioManager?.enterForeground = { - Log.d(TAG, "enter foreground=$it") - val intent = Intent(this, PlayerService::class.java) - intent.putExtra(KEY_ENTER_FOREGROUND, it) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - - val mediaInfo = registerResult?.getParcelable(Keys.API_PARAM_KEY_MEDIA_INFO) - mediaInfo ?: return - Log.d(TAG, "initData, mediaInfo=$mediaInfo") - - audioManager?.let { manager -> - manager.stopPlay() - manager.initData( - mediaInfo.sampleRateInHz, - if (mediaInfo.channelConfig == 2) AudioFormat.CHANNEL_IN_STEREO else AudioFormat.CHANNEL_IN_MONO, - mediaInfo.audioFormat - ) - - val parcelFileDescriptor = - registerResult.getParcelable(Keys.API_PARAM_KEY_PCM_FILE_DESCRIPTOR) - parcelFileDescriptor?.let { - Log.d(TAG, "parcelFileDescriptor: $it") - manager.startPlayByFileDescriptor(it) - } - } - } - - private fun onMediaInfoChangedFloat(registerResult: Bundle?) { - audioManagerFloat?.stopPlay() - audioManagerFloat = AudioTrackManager() - - audioManagerFloat?.printMessageCallback = { - printer?.print(it) - } - - audioManagerFloat?.enterForeground = { - Log.d(TAG, "enter foreground=$it") - val intent = Intent(this, PlayerService::class.java) - intent.putExtra(KEY_ENTER_FOREGROUND, it) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - } - - val mediaInfo = registerResult?.getParcelable(Keys.API_PARAM_KEY_MEDIA_INFO) - mediaInfo ?: return - Log.d(TAG, "initData, mediaInfo=$mediaInfo") - - audioManagerFloat?.let { manager -> - manager.stopPlay() - manager.initData( - mediaInfo.sampleRateInHz, - if (mediaInfo.channelConfig == 2) AudioFormat.CHANNEL_IN_STEREO else AudioFormat.CHANNEL_IN_MONO, - mediaInfo.audioFormat - ) - - val parcelFileDescriptor = - registerResult.getParcelable(Keys.API_PARAM_KEY_PCM_FILE_DESCRIPTOR_FLOAT) - parcelFileDescriptor?.let { - Log.d(TAG, "parcelFileDescriptor: $it") - manager.startPlayByFileDescriptor(it) - } - } - } - - private fun bindService() { - qPlayBindHelper.ensureQQMusicBindByStartProcess { success -> - if (success && qPlayBindHelper.isBindQQMusic()) { - qqMusicApi = qPlayBindHelper.getQQMusicApi() - - val bundle = Bundle().apply { - putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) - putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) - } - val result = qqMusicApi?.execute("hi", bundle) - val code = result?.getInt(Keys.API_RETURN_KEY_CODE) - if (code == ErrorCodes.ERROR_API_NO_PERMISSION) { - val time = System.currentTimeMillis() - val nonce = time.toString() - val encryptString = OpenIDHelper.getEncryptString(nonce) - CommonCmd.verifyCallerIdentity( - this, - Config.OPENID_APPID, - packageName, - encryptString, - "eaimar://" - ) - } else { - Toast.makeText(this@PlayerService, "授权成功", Toast.LENGTH_SHORT).show() - qqMusicApi?.registerEventListener( - arrayListOf( - Events.API_EVENT_PLAY_STATE_CHANGED, - Events.API_EVENT_PLAY_SONG_CHANGED, - Events.API_EVENT_MEDIA_INFO_CHANGED - ), eventListener - ) - } - } - } - } - - private fun isPlaying(): Boolean { - Log.d(TAG, "[isPlaying]curPlayState:$curPlayState") - return (curPlayState == PlayState.STARTED - || curPlayState == PlayState.INITIALIZED - || curPlayState == PlayState.PREPARED - || curPlayState == PlayState.PREPARING - || curPlayState == PlayState.BUFFERING) - } -} - -/** - * Bundle 扩展函数,用于将 Bundle 转换为可打印的字符串 - */ -fun Bundle.toPrintableString(): String { - val sb = StringBuilder() - sb.append("{") - - for (key in keySet()) { - if (sb.length > 1) { - sb.append(", ") - } - - val value = get(key) - sb.append(key).append("=") - - when (value) { - is String -> sb.append("\"$value\"") - is Bundle -> sb.append(value.toPrintableString()) - is Array<*> -> sb.append(value.contentToString()) - is IntArray -> sb.append(value.contentToString()) - is LongArray -> sb.append(value.contentToString()) - is FloatArray -> sb.append(value.contentToString()) - is DoubleArray -> sb.append(value.contentToString()) - is BooleanArray -> sb.append(value.contentToString()) - is ByteArray -> sb.append(value.contentToString()) - is CharArray -> sb.append(value.contentToString()) - is ShortArray -> sb.append(value.contentToString()) - else -> sb.append(value?.toString() ?: "null") - } - } - - sb.append("}") - return sb.toString() -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java deleted file mode 100644 index bf8ba5bd..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.yunqiinnovation.qq_music.util; - -/** - * Created by qitianliu on 2022/8/8. - */ -public class BitConverter { - public static int intFromByteArray(byte[] sizeBuf, int offset) { - int ret = 0; - - for(int i = offset; i < offset + 4; ++i) { - ret <<= 8; - ret |= 255 & sizeBuf[i]; - } - - return ret; - } -} diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt deleted file mode 100644 index 85c319ee..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt +++ /dev/null @@ -1,218 +0,0 @@ -package com.yunqiinnovation.qq_music.util - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.content.pm.PackageManager -import android.os.Handler -import android.os.IBinder -import android.os.Looper -import android.util.Log -import android.widget.Toast -import com.tencent.qqmusic.third.api.contract.CommonCmd -import com.tencent.qqmusic.third.api.contract.CommonCmd.AIDL_PLATFORM_TYPE_PHONE -import com.tencent.qqmusic.third.api.contract.IQQMusicApi -import com.yunqiinnovation.qq_music.Config - -class QPlayBindHelper(private val context: Context, private val bindPlatform: String = AIDL_PLATFORM_TYPE_PHONE) { - companion object { - private const val TAG = "QQMusicBindHelper" - } - - private var api: IQQMusicApi? = null - - private val handler: Handler by lazy { Handler(Looper.getMainLooper()) } - - private var reBinding = false - private val deathRecipient by lazy { - IBinder.DeathRecipient { - Log.e(TAG, "[binderDied] ") - api = null - if (reBinding.not()) { - //reBindQQMusicService() - } - } - } - - private val conn = object : ServiceConnection { - var success: (() -> Unit)? = null - override fun onServiceDisconnected(name: ComponentName?) { - Log.e(TAG, "[onServiceDisconnected] ") - api = null - if (reBinding.not()) { - //reBindQQMusicService() - } - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - api = IQQMusicApi.Stub.asInterface(service) - // 注册死亡代理 - try { - service?.linkToDeath(deathRecipient, 0) - } catch (e: Exception) { - Log.e(TAG, "[onServiceConnected] link to death exception ", e) - } - success?.invoke() - success = null - } - } - - fun bindQQMusic(callback: ((Boolean) -> Unit)? = null, retry: Boolean = false) { - if (isBindQQMusic()) { - callback?.invoke(true) - } - // 必须显式绑定 - val intent = getQQMusicApiServiceIntent() - - val ret = try { - Log.d(TAG, "[bindQQMusic] bindService retry:$retry") - context.bindService(intent, conn, Context.BIND_AUTO_CREATE) - } catch (e: Exception) { - Log.e(TAG, "startBindQQMusic failed") - false - } - if (!ret) { - // 绑定失败,可能是qq音乐进程未启动,或者可能是魅族系列手机,需要先startService,才能bind - if (retry.not()) { - val startComponent = try { - // 如果qq音乐进程没有启动,高版本的Android系统start service会返回null - context.startService(intent) - } catch (e: Throwable) { - } - if (startComponent != null) { - bindQQMusic(callback, true) - } else { - callback?.invoke(false) - } - } else { - callback?.invoke(false) - } - } else { - conn.success = { - callback?.invoke(true) - } - } - } - - fun unBindQQMusic() { - context.unbindServiceSafe(conn) - } - - /** - * bindService不成功时,调用一次startService,但还是需要调用方延迟后进行重试 - */ - fun ensureQQMusicBind(callback: ((Boolean) -> Unit)? = null) { - bindQQMusic(callback) - } - - /** - * 检查QQ音乐App是否已安装 - */ - private fun isQQMusicInstalled(): Boolean { - return try { - val packageName = getQQMusicPackageName() - context.packageManager.getPackageInfo(packageName, PackageManager.GET_ACTIVITIES) - true - } catch (e: PackageManager.NameNotFoundException) { - Log.w(TAG, "QQ音乐App未安装: ${e.message}") - false - } - } - - /** - * 获取QQ音乐包名 - */ - private fun getQQMusicPackageName(): String { - return when (Config.BIND_PLATFORM) { - CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic" - CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar" - CommonCmd.AIDL_PLATFORM_TYPE_TV -> "com.tencent.qqmusictv" - else -> "com.tencent.qqmusic" - } - } - - fun ensureQQMusicBindByStartProcess(isStartProcess: Boolean = true, callback: ((Boolean) -> Unit)? = null) { - // 首先检查QQ音乐是否已安装 - if (!isQQMusicInstalled()) { - Log.e(TAG, "QQ音乐App未安装,无法绑定服务") - callback?.invoke(false) - return - } - - bindQQMusic({ succeed -> - if (!succeed) { - if (isStartProcess) { - try { - CommonCmd.startQQMusicProcess(context, context.packageName) - } catch (e: Exception) { - Log.e(TAG, "启动QQ音乐进程失败: ${e.message}") - callback?.invoke(false) - return@bindQQMusic - } - } - handler.postDelayed({ - ensureQQMusicBindByStartProcess(false, callback) - }, 800) - } else { - callback?.invoke(true) - } - }, true) - } - - fun isBindQQMusic() = api != null - - fun getQQMusicApi() = api - - private fun getQQMusicApiServiceIntent(): Intent { - var intent = Intent("com.tencent.qqmusic.third.api.QQMusicApiService") - // 必须显式绑定 - when (bindPlatform) { - CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> { - intent = Intent("com.tencent.qqmusic.third.api.QQMusicApiService") - intent.`package` = "com.tencent.qqmusic" - } - CommonCmd.AIDL_PLATFORM_TYPE_CAR -> { - intent = Intent("com.tencent.qqmusiccar.third.api.QQMusicApiService") - intent.`package` = "com.tencent.qqmusiccar" - } - CommonCmd.AIDL_PLATFORM_TYPE_TV -> { - intent = Intent("com.tencent.qqmusictv.third.api.QQMusicApiService") - intent.`package` = "com.tencent.qqmusictv" - } - else -> { - Log.e(TAG, "platform 不匹配") - Toast.makeText(context, "请先在Config中填写配置信息!", Toast.LENGTH_SHORT).show() - } - } - return intent - } - - /** - * 重新绑定qq音乐 - */ - private fun reBindQQMusicService() { - CommonCmd.startQQMusicProcess(context, context.packageName) - handler.postDelayed(Runnable { - ensureQQMusicBind { - if (it) { - Log.e(TAG, "重新绑定成功") - reBinding = false - } else { - Log.e(TAG, "重新绑定失败") - reBindQQMusicService() - } - } - }, 200) - reBinding = true - } - -} - -fun Context.unbindServiceSafe(conn: ServiceConnection) { - try { - unbindService(conn) - } catch (e: Throwable) { - Log.e("CommExt", "unbindService failed", e) - } -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt b/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt deleted file mode 100644 index c1af8dae..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.yunqiinnovation.qq_music.util - -import android.content.Context -import android.os.Bundle -import android.os.DeadObjectException -import android.util.Log -import com.yunqiinnovation.qq_music.Config -import com.yunqiinnovation.qq_music.QQMusicLoginManager.Companion.LOGIN_FAILED -import com.yunqiinnovation.qq_music.openid.OpenIDHelper -import com.tencent.qqmusic.third.api.contract.* - -class QQMusicApiWrapper(val qqMusicApi: IQQMusicApi) { - companion object { - const val TAG = "QQMusicApiWrapper" - - private const val QQMUSIC_VERSION_10_3_0_1 = 10030001 - - private var qqMusicVersion: Int = -1 - - } - - fun verifyCallerIdentity(context: Context, callbackUri: String?) { - val time = System.currentTimeMillis() - val nonce = time.toString() - val encryptString = OpenIDHelper.getEncryptString(nonce) - - // 添加调试日志 - Log.d(TAG, "verifyCallerIdentity - APPID: ${Config.OPENID_APPID}") - Log.d(TAG, "verifyCallerIdentity - PackageName: ${context.packageName}") - Log.d(TAG, "verifyCallerIdentity - CallbackUri: $callbackUri") - Log.d(TAG, "verifyCallerIdentity - EncryptString: $encryptString") - - CommonCmd.verifyCallerIdentity( - context, - Config.OPENID_APPID, - context.packageName, - encryptString, - callbackUri - ) - } - - /** - * qq音乐10.3以后才在 DispacherActivityForThird 中支持 onNewIntent - */ - fun isQQMusicCanHandleOnNewIntent() = getQQMusicVersion() >= QQMUSIC_VERSION_10_3_0_1 - - fun getQQMusicVersion(): Int { - if (qqMusicVersion != -1) { - return qqMusicVersion - } - val bundle = Bundle() - bundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) - bundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, CommonCmd.AIDL_PLATFORM_TYPE_PHONE) - val result = try { - qqMusicApi.execute("hi", bundle) - } catch (e: Exception) { - null - } - qqMusicVersion = result?.getInt(Keys.API_RETURN_KEY_VERSION) ?: -1 - return qqMusicVersion - } - - - /** - * 申请授权,[success]返回授权成功后的加密数据;[failed]返回错误码和错误说明,错误码参考[LOGIN_FAILED]等常量 - */ - fun requestAuthNew(success: (String) -> Unit, failed: (Int, String?) -> Unit) { - Log.i(TAG, "[requestAuth] ") - val time = System.currentTimeMillis() - val nonce = time.toString() - val encryptString = OpenIDHelper.getEncryptString(nonce) //解密&加密 - val params = Bundle() - params.putString(Keys.API_RETURN_KEY_ENCRYPT_STRING, encryptString) - - try { - qqMusicApi.executeAsync("requestAuth", params, object : IQQMusicApiCallback.Stub() { - override fun onReturn(result: Bundle) { - val code = result.getInt(Keys.API_RETURN_KEY_CODE) - Log.i(TAG, "[onReturn] code $code") - if (code == ErrorCodes.ERROR_OK) { - val qmEncryptString = - result.getString(Keys.API_RETURN_KEY_ENCRYPT_STRING) - if (qmEncryptString != null) { - success(qmEncryptString) - } else { - failed(LOGIN_FAILED, "qmEncryptString is null") - } - } else { - val errorMsg = result.getString(Keys.API_RETURN_KEY_ERROR) - failed(LOGIN_FAILED, errorMsg) - } - } - }) - } catch (e: Exception) { - if (e is DeadObjectException) { - Log.e(TAG, "[requestAuthInternal] dead object exception ", e) - failed(LOGIN_FAILED, "service is dead") - } else { - Log.e(TAG, "[requestAuthInternal] other exception ", e) - failed(LOGIN_FAILED, "other exception[${e.message}]") - } - } - } - -} \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml b/apps/client/local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 5713f34a..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,171 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml b/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml deleted file mode 100644 index 6ce52a76..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml b/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml deleted file mode 100644 index 23588041..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - \ No newline at end of file diff --git a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml b/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml deleted file mode 100644 index 404d4e39..00000000 --- a/apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - -