Browse Source

清理:QQ 音乐整体下线(插件、Dart 服务、灵犀 AI 里的 QQ 音乐工具、iOS 回跳)

用户决定整个删掉(2026-09-24)。

- 删 local_plugins/qq_music 整个插件(含 QPlayAutoSDK 与 Android aidl aar,约 3.5MB);
  pubspec、android settings.gradle、根 build.gradle 与 app build.gradle 里的 flatDir 引用同步去掉
- Dart:QqMusicService(qq_music.dart)删除;翻译页 suspend/restoreOtherServices 里暂停/恢复 QQ 音乐的分支去掉;
  main.dart 的 deepvoice://qqmusic 深链接分支与 SplashController.handleQQMusicLogin 去掉
- iOS Runner:AppDelegate 里把 eaimar:// 转给 "qq_music" 通道的那段去掉(该通道名本来就和插件
  实际注册的 com.yunqiinnovation.qq_music 对不上,回跳一直是空转);LSApplicationQueriesSchemes 去掉 qqmusic
- 灵犀/移动精灵 AI(原生):
  - chat_api 两端删 QQMusicSystemFunctionHandler(9 个 qqmusic_* 工具);MCPClient 的 "QQ"/未指定/AUTO
    一律走网易/系统音乐工具
  - agent_service 两端删 qqmusic_searchandplay_songs 的播放处理、Android 的 QQMusicSingleton 初始化与
    autopause/resume 的 QQ 分支;Package.swift / build.gradle 的 qq_music 依赖去掉
  - 行为变化:Dart 从未传 musicServiceType,Android 原本就不注册音乐工具、QQ 分支走不到,无变化;
    iOS 原本「装了 QQ 音乐就给 AI 用 QQ 工具」,现在一律用网易/系统音乐工具

保留:MusicManager(翻译时暂停其它 App 的音乐,走 music_service,与 QQ 无关);
eaimar:// URL scheme 注册(App 级深链接入口,只去掉转发给 QQ 的那步)。

验证:dart analyze 无 error、无 unused_import;flutter test 221 全过;flutter build ios --no-codesign 通过。
Android 编译同第 4 批,受本机缺 Rosetta 影响未验证。

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 362a3ac70aac2d79d0483076997949c8b2dac2a9)
main
Rodger-Wang 2 weeks ago
parent
commit
18a7160183
  1. 3
      apps/client/android/app/build.gradle.kts
  2. 5
      apps/client/android/build.gradle.kts
  3. 2
      apps/client/android/settings.gradle.kts
  4. 14
      apps/client/ios/Runner/AppDelegate.swift
  5. 1
      apps/client/ios/Runner/Info.plist
  6. 4
      apps/client/lib/core/bindings/initial_binding.dart
  7. 424
      apps/client/lib/data/services/qq_music.dart
  8. 28
      apps/client/lib/main.dart
  9. 15
      apps/client/lib/modules/splash/controllers/splash_controller.dart
  10. 10
      apps/client/lib/modules/translation/controllers/translation_controller.dart
  11. 1
      apps/client/local_plugins/agent_service/android/build.gradle.kts
  12. 90
      apps/client/local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  13. 2
      apps/client/local_plugins/agent_service/ios/agent_service/Package.swift
  14. 30
      apps/client/local_plugins/agent_service/ios/agent_service/Sources/agent_service/AgentServiceImpl.swift
  15. 1
      apps/client/local_plugins/chat_api/android/build.gradle.kts
  16. 22
      apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/MCPClient.kt
  17. 460
      apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt
  18. 2
      apps/client/local_plugins/chat_api/ios/chat_api/Package.swift
  19. 18
      apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift
  20. 332
      apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift
  21. 33
      apps/client/local_plugins/qq_music/.gitignore
  22. 33
      apps/client/local_plugins/qq_music/.metadata
  23. 3
      apps/client/local_plugins/qq_music/CHANGELOG.md
  24. 1
      apps/client/local_plugins/qq_music/LICENSE
  25. 15
      apps/client/local_plugins/qq_music/README.md
  26. 4
      apps/client/local_plugins/qq_music/analysis_options.yaml
  27. 9
      apps/client/local_plugins/qq_music/android/.gitignore
  28. 79
      apps/client/local_plugins/qq_music/android/build.gradle
  29. BIN
      apps/client/local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar
  30. 1
      apps/client/local_plugins/qq_music/android/settings.gradle
  31. 9
      apps/client/local_plugins/qq_music/android/src/main/AndroidManifest.xml
  32. 15
      apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl
  33. 8
      apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl
  34. 53
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java
  35. 306
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt
  36. 726
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  37. 690
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt
  38. 237
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt
  39. 64
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java
  40. 314
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java
  41. 253
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt
  42. 403
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt
  43. 17
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java
  44. 218
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt
  45. 105
      apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt
  46. 171
      apps/client/local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml
  47. 8
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml
  48. 17
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml
  49. 20
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml
  50. 170
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_main.xml
  51. 48
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_pcm_example.xml
  52. 220
      apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_visual.xml
  53. 21
      apps/client/local_plugins/qq_music/android/src/main/res/layout/folder_list_view_item.xml
  54. 45
      apps/client/local_plugins/qq_music/android/src/main/res/layout/song_list_view_item.xml
  55. 24
      apps/client/local_plugins/qq_music/android/src/main/res/layout/view_popview_more.xml
  56. 14
      apps/client/local_plugins/qq_music/android/src/main/res/menu/toolbar_menu.xml
  57. BIN
      apps/client/local_plugins/qq_music/android/src/main/res/mipmap-hdpi/mini_album_default.png
  58. 18
      apps/client/local_plugins/qq_music/android/src/main/res/values/array.xml
  59. 7
      apps/client/local_plugins/qq_music/android/src/main/res/values/colors.xml
  60. 4
      apps/client/local_plugins/qq_music/android/src/main/res/values/dimens.xml
  61. 12
      apps/client/local_plugins/qq_music/android/src/main/res/values/strings.xml
  62. 20
      apps/client/local_plugins/qq_music/android/src/main/res/values/styles.xml
  63. 38
      apps/client/local_plugins/qq_music/ios/.gitignore
  64. 33
      apps/client/local_plugins/qq_music/ios/qq_music.podspec
  65. 47
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/Info.plist
  66. 238
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/QPlayAutoDefine.h
  67. 188
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/QPlayAutoSDK.h
  68. 8
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/module.modulemap
  69. BIN
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/libQPlayAutoSDK_arm64.a
  70. 238
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/QPlayAutoDefine.h
  71. 188
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/QPlayAutoSDK.h
  72. 8
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/module.modulemap
  73. BIN
      apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/libQPlayAutoSDK_x86_64.a
  74. 26
      apps/client/local_plugins/qq_music/ios/qq_music/Package.swift
  75. 932
      apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift
  76. 536
      apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QqMusicPlugin.swift
  77. 238
      apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/QPlayAutoDefine.h
  78. 188
      apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/QPlayAutoSDK.h
  79. 5
      apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/module.modulemap
  80. 311
      apps/client/local_plugins/qq_music/lib/qq_music.dart
  81. 28
      apps/client/local_plugins/qq_music/pubspec.yaml
  82. 7
      apps/client/pubspec.lock
  83. 2
      apps/client/pubspec.yaml

3
apps/client/android/app/build.gradle.kts

@ -3,9 +3,6 @@ import java.io.FileInputStream
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
flatDir {
dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行
}
} }
plugins { plugins {

5
apps/client/android/build.gradle.kts

@ -5,11 +5,6 @@ allprojects {
mavenCentral() mavenCentral()
maven { url = uri("https://storage.googleapis.com/download.flutter.io") } maven { url = uri("https://storage.googleapis.com/download.flutter.io") }
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") } maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
// 修改为绝对路径,避免相对路径计算错误
flatDir {
dirs(file("${rootProject.projectDir}/../local_plugins/qq_music/android/libs"))
}
} }
} }

2
apps/client/android/settings.gradle.kts

@ -39,7 +39,6 @@ include(":classic_bluetooth")
include(":agent_service") include(":agent_service")
include(":speech") include(":speech")
include(":chat_api") include(":chat_api")
include(":qq_music") // 添加这行
include(":bluetooth_manager") // 恒玄(BES)经典蓝牙连接 + G722/mSBC 音频编解码 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(":agent_service").projectDir = file("../local_plugins/agent_service/android")
project(":speech").projectDir = file("../local_plugins/speech/android") project(":speech").projectDir = file("../local_plugins/speech/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/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") project(":bluetooth_manager").projectDir = file("../local_plugins/bluetooth_manager/android")

14
apps/client/ios/Runner/AppDelegate.swift

@ -30,20 +30,6 @@ import UIKit
return true 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 return false
} }
} }

1
apps/client/ios/Runner/Info.plist

@ -65,7 +65,6 @@
<array> <array>
<string>App-Prefs</string> <string>App-Prefs</string>
<string>prefs</string> <string>prefs</string>
<string>qqmusic</string>
<string>weixin</string> <string>weixin</string>
<string>weixinULAPI</string> <string>weixinULAPI</string>
<string>weixinURLParamsAPI</string> <string>weixinURLParamsAPI</string>

4
apps/client/lib/core/bindings/initial_binding.dart

@ -1,6 +1,5 @@
import '../../data/services/meeting/meeting_task_service.dart'; import '../../data/services/meeting/meeting_task_service.dart';
import '/data/services/pay_service.dart'; import '/data/services/pay_service.dart';
import '/data/services/qq_music.dart';
import '../../../data/services/user_portrait.dart'; import '../../../data/services/user_portrait.dart';
@ -132,10 +131,7 @@ class InitialBinding extends Bindings {
Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true); Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true);
// qq音乐服务 // qq音乐服务
Get.lazyPut<QqMusicService>(() => QqMusicService(), fenix: true);
// 强制初始化 qq_music 插件
// Get.find<QqMusicService>().initializePlugin();
// 新手引导 // 新手引导
Get.lazyPut<NoviceGuideService>(() => NoviceGuideService(), fenix: true); Get.lazyPut<NoviceGuideService>(() => NoviceGuideService(), fenix: true);

424
apps/client/lib/data/services/qq_music.dart

@ -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<QqMusicService>();
var isInitialized = false;
// 音乐服务实例
final QqMusic _qqMusic = QqMusic();
// 全局音乐状态
final isPlaying = false.obs;
final currentPosition = 0.obs; // 毫秒
final totalDuration = 0.obs; // 毫秒
final currentSong = Rxn<SongModel>();
// 状态监听定时器
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<void> _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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> playSongs({
required List<String> songMids,
int index = 0,
List<SongModel>? 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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<bool> autopauseMusic() async {
try {
final result = await _qqMusic.autopauseMusic();
if (result['success'] == true) {
isPlaying.value = false;
}
return result['success'] == true;
} catch (e) {
return false;
}
}
/// 继续播放
Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> skipToNext() async {
try {
final result = await _qqMusic.skipToNext();
if (result['success'] == true) {}
return result;
} catch (e) {
return {'success': false, 'error': '下一首失败: ${e.toString()}'};
}
}
/// 上一首
Future<Map<String, dynamic>> skipToPrevious() async {
try {
final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {}
return result;
} catch (e) {
return {'success': false, 'error': '上一首失败: ${e.toString()}'};
}
}
/// 获取播放状态
Future<Map<String, dynamic>> getPlaybackState() async {
try {
final result = await _qqMusic.getPlaybackState();
return result;
} catch (e) {
return {'success': false, 'error': '获取播放状态失败: ${e.toString()}'};
}
}
/// 获取当前播放歌曲
Future<Map<String, dynamic>> getCurrentSong() async {
try {
final result = await _qqMusic.getCurrentSong();
return result;
} catch (e) {
return {'success': false, 'error': '获取当前歌曲失败: ${e.toString()}'};
}
}
/// 获取播放时间
Future<Map<String, dynamic>> getPlayTime() async {
try {
final result = await _qqMusic.getPlayTime();
return result;
} catch (e) {
return {'success': false, 'error': '获取播放时间失败: ${e.toString()}'};
}
}
/// 同步当前播放信息 - 统一接口
Future<Map<String, dynamic>> 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()}'};
}
}
}

28
apps/client/lib/main.dart

@ -24,7 +24,6 @@ import 'core/services/native_localization_sync.dart';
import 'core/services/ai_orb_controller.dart'; import 'core/services/ai_orb_controller.dart';
import 'data/services/login_service.dart'; import 'data/services/login_service.dart';
import 'modules/meeting/controllers/meeting_home_controller.dart'; import 'modules/meeting/controllers/meeting_home_controller.dart';
import 'modules/splash/controllers/splash_controller.dart';
bool _deepLinkHandlerInitialized = false; bool _deepLinkHandlerInitialized = false;
@ -109,32 +108,7 @@ Future<void> _handleDeepLinkNavigation(String url) async {
Logger.d("DeepLink", Logger.d("DeepLink",
"解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}"); "解析URI: scheme=${uri.scheme}, host=${uri.host}, path=${uri.path}");
if (uri.scheme == 'deepvoice' && uri.host == 'qqmusic') { // 目前没有需要处理的深链接(原 deepvoice://qqmusic 的 QQ 音乐登录回调已随 QQ 音乐一起下线)。
Logger.i("DeepLink", "检测到QQ音乐登录回调");
// 等待SplashController注册
int retryCount = 0;
const maxRetries = 10;
while (retryCount < maxRetries) {
try {
if (Get.isRegistered<SplashController>()) {
Logger.i("DeepLink", "找到SplashController,处理QQ音乐登录");
final controller = Get.find<SplashController>();
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注册");
}
} catch (e) { } catch (e) {
Logger.e("DeepLink", "处理深链接时出错", e); Logger.e("DeepLink", "处理深链接时出错", e);
} }

15
apps/client/lib/modules/splash/controllers/splash_controller.dart

@ -326,21 +326,6 @@ class SplashController extends GetxController {
Logger.e(_tag, 'initMeetingTemplate 获取失败: $e'); 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');
}
}
} }
/// 显示无网络连接对话框并等待用户操作(阻塞版本) /// 显示无网络连接对话框并等待用户操作(阻塞版本)

10
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:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.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/ast_service.dart';
import '../../../data/services/server_translation_service.dart'; import '../../../data/services/server_translation_service.dart';
import '../../../data/services/music_manager.dart'; import '../../../data/services/music_manager.dart';
@ -122,7 +121,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
static const int _astReconnectMaxDelaySec = 8; static const int _astReconnectMaxDelaySec = 8;
final GetStorage _storage = GetStorage(); final GetStorage _storage = GetStorage();
final _musiceManager = Get.find<MusicManager>(); final _musiceManager = Get.find<MusicManager>();
final _qqMusicManager = Get.find<QqMusicService>();
late final UsageStatsService _usageService; late final UsageStatsService _usageService;
// 安卓悬浮窗管理器 // 安卓悬浮窗管理器
late final FloatingWindowManager _floatingWindowManager; late final FloatingWindowManager _floatingWindowManager;
@ -141,7 +139,6 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
bool _audioSourceType = false; // 音频输入源类型 bool _audioSourceType = false; // 音频输入源类型
bool isPlayback = false; // 是否开启播放 bool isPlayback = false; // 是否开启播放
bool isContinuePlaying = false; // 是否继续播放 bool isContinuePlaying = false; // 是否继续播放
bool _isContinueQqMusicPlaying = false; // 是否继续播放QQ音乐
// 存储相关配置 // 存储相关配置
static const String _sourceLanguageKey = 'translation_source_language'; 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) { } catch (e) {
Logger.error('暂停其他服务失败: ${e.toString()}'); Logger.error('暂停其他服务失败: ${e.toString()}');
} }
@ -1010,12 +1002,10 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
Future<void> restoreOtherServices() async { Future<void> restoreOtherServices() async {
try { try {
if (isContinuePlaying) await _musiceManager.resume(); if (isContinuePlaying) await _musiceManager.resume();
if (_isContinueQqMusicPlaying) await _qqMusicManager.resumeMusic();
} catch (e) { } catch (e) {
Logger.error('恢复其他服务失败: ${e.toString()}'); Logger.error('恢复其他服务失败: ${e.toString()}');
} finally { } finally {
isContinuePlaying = false; isContinuePlaying = false;
_isContinueQqMusicPlaying = false;
} }
} }

1
apps/client/local_plugins/agent_service/android/build.gradle.kts

@ -52,6 +52,5 @@ dependencies {
add("compileOnly", project(":chat_api")) add("compileOnly", project(":chat_api"))
add("compileOnly", project(":location_service")) add("compileOnly", project(":location_service"))
add("compileOnly", project(":user_portrait")) add("compileOnly", project(":user_portrait"))
add("compileOnly", project(":qq_music"))
} }

90
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.amap_service.NavigationServiceHelper
import com.yunqiinnovation.user_portrait.UserPortrait import com.yunqiinnovation.user_portrait.UserPortrait
import com.yunqiinnovation.location_service.LocationService import com.yunqiinnovation.location_service.LocationService
import com.yunqiinnovation.qq_music.QQMusicSingleton
import android.util.Log import android.util.Log
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.util.Collections import java.util.Collections
@ -544,7 +543,6 @@ object AgentService : CoroutineScope {
//初始化定位服务 //初始化定位服务
LocationService.initialize(appContext) LocationService.initialize(appContext)
MusicSingleton.getInstance().initialize(appContext,serverurl,usertoken) MusicSingleton.getInstance().initialize(appContext,serverurl,usertoken)
QQMusicSingleton.getInstance().initialize(appContext)
// 设置系统提示词 // 设置系统提示词
val customSystemPrompt = config["systemPrompt"] as? String val customSystemPrompt = config["systemPrompt"] as? String
//保存配置 //保存配置
@ -1660,7 +1658,7 @@ object AgentService : CoroutineScope {
if (name == "exit_interaction") { if (name == "exit_interaction") {
BleService.closeCodec() BleService.closeCodec()
stopRecognition() 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 isplaymusic = false
} }
_log("INFO", "[Agent] AI onFunctionCall: callingTool=$iscallingTool") _log("INFO", "[Agent] AI onFunctionCall: callingTool=$iscallingTool")
@ -2572,40 +2570,7 @@ object AgentService : CoroutineScope {
} else { } else {
Log.w(TAG, "card_musiclist.musics 不是一个数组") Log.w(TAG, "card_musiclist.musics 不是一个数组")
} }
} else if (toolname == "qqmusic_search_songs" && meta.has("data")) { //qq音乐 } else if (meta.has("card_navigation")) { //导航服务
//"{\"mid\": \"$mid\", \"name\": \"$name\", \"artist\": \"$artist\", \"album\": \"$album\", \"duration\":$duration,\"pic\": \"$pic\"}"
val musics = meta.optJSONArray("data")
if (musics != null) {
val playlist = mutableListOf<Map<String, Any>>()
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")) { //导航服务
val cardNavigation = meta.getJSONObject("card_navigation") val cardNavigation = meta.getJSONObject("card_navigation")
val start = cardNavigation.optString("start", "") val start = cardNavigation.optString("start", "")
val destination = cardNavigation.optString("destination", "") val destination = cardNavigation.optString("destination", "")
@ -2672,39 +2637,6 @@ object AgentService : CoroutineScope {
Log.e(TAG, "音乐列表为空,未启动播放服务") Log.e(TAG, "音乐列表为空,未启动播放服务")
} }
} }
/**
* 处理音乐播放列表
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processQQMusicPlayList(songs: List<Map<String, Any>>) {
// 如果 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(){ private fun autopauseMusic(){
_log("INFO", "[Agent][音乐] autopauseMusic 进入 type=$musicServiceType isplaymusic=$isplaymusic") _log("INFO", "[Agent][音乐] autopauseMusic 进入 type=$musicServiceType isplaymusic=$isplaymusic")
if ( musicServiceType == "QQ" ){ if ( musicServiceType == "WY" ){
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 (MusicService.isPlaying()){ if (MusicService.isPlaying()){
MusicSingleton.startServiceWithCommand(context, "pause") MusicSingleton.startServiceWithCommand(context, "pause")
isplaymusic = true isplaymusic = true
@ -3258,7 +3181,7 @@ object AgentService : CoroutineScope {
_log("INFO", "[Agent][音乐] WY 当前未在播放,跳过暂停") _log("INFO", "[Agent][音乐] WY 当前未在播放,跳过暂停")
} }
} else { } 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") _log("INFO", "[Agent][音乐] resumeMusic 进入 type=$musicServiceType isplaymusic=$isplaymusic")
if ( isplaymusic ){ if ( isplaymusic ){
isplaymusic = false isplaymusic = false
if ( musicServiceType == "QQ" ){ if ( musicServiceType == "WY" ){
_log("INFO", "[Agent][音乐] 调用 QQMusic.resumeMusic")
QQMusicSingleton.getInstance().resumeMusic {}
}else if ( musicServiceType == "WY" ){
_log("INFO", "[Agent][音乐] 调用 WY MusicSingleton resume") _log("INFO", "[Agent][音乐] 调用 WY MusicSingleton resume")
MusicSingleton.startServiceWithCommand(context, "resume") MusicSingleton.startServiceWithCommand(context, "resume")
} else { } else {

2
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: "../../../location_service/ios/location_service"),
.package(path: "../../../music_service/ios/music_service"), .package(path: "../../../music_service/ios/music_service"),
.package(path: "../../../user_portrait/ios/user_portrait"), .package(path: "../../../user_portrait/ios/user_portrait"),
.package(path: "../../../qq_music/ios/qq_music"),
], ],
targets: [ targets: [
.target( .target(
@ -30,7 +29,6 @@ let package = Package(
.product(name: "location-service", package: "location_service"), .product(name: "location-service", package: "location_service"),
.product(name: "music-service", package: "music_service"), .product(name: "music-service", package: "music_service"),
.product(name: "user-portrait", package: "user_portrait"), .product(name: "user-portrait", package: "user_portrait"),
.product(name: "qq-music", package: "qq_music"),
], ],
path: "Sources/agent_service", path: "Sources/agent_service",
resources: [ resources: [

30
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 chat_storage
import music_service import music_service
import user_portrait import user_portrait
import qq_music
protocol AgentServiceListener: AnyObject { protocol AgentServiceListener: AnyObject {
func onEvent(eventName: String, data: [String: Any]) 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] { } else if let cardNavigation = jsonDict["card_navigation"] as? [String: Any] {
let isinternal = processNavigationCard(cardNavigation) let isinternal = processNavigationCard(cardNavigation)
metadata["isinternal"] = isinternal metadata["isinternal"] = isinternal
} else if let cardQQMusiclist = jsonDict["data"] as? [[String: Any]], toolName == "qqmusic_searchandplay_songs" {
processQQMusicPlayList(cardQQMusiclist)
} else if toolName == "map_weather" { } else if toolName == "map_weather" {
iscard = true iscard = true
}else if toolName == "maps_weather" { }else if toolName == "maps_weather" {
@ -1232,31 +1229,6 @@ private func jsonToString(_ json: [String: Any]) -> String? {
return nil 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]) { private func processMusicCard(_ cardMusic: [String: Any]) {
let id = cardMusic["id"] as? Int ?? 0 let id = cardMusic["id"] as? Int ?? 0
@ -2100,7 +2072,7 @@ class ChatApiStreamCallback: StreamCallback {
// 关闭编码器 (设备侧) // 关闭编码器 (设备侧)
BleService.shared.closeCodec() BleService.shared.closeCodec()
agentService.stopRecognition() 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 agentService.wasMusicPlayingBeforeRecognition = false
} }

1
apps/client/local_plugins/chat_api/android/build.gradle.kts

@ -61,5 +61,4 @@ dependencies {
add("compileOnly", project(":location_service")) add("compileOnly", project(":location_service"))
add("compileOnly", project(":music_service")) add("compileOnly", project(":music_service"))
add("compileOnly", project(":qq_music"))
} }

22
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") Log.d(TAG, "初始化音乐服务: $musicType")
when (musicType) { when (musicType) {
"WY" -> initializeMusiceFunctions() //网易 "WY" -> initializeMusiceFunctions() //网易
"QQ" -> initializeQQMusiceFunctions() //QQ // QQ 音乐已于 2026-09-24 下线,"QQ"/"AUTO" 一律走网易
"AUTO" -> { "QQ", "AUTO" -> initializeMusiceFunctions()
if (QQMusicSystemFunctionHandler.CheckQQMusicInstall()) { //检查是否安装qq音乐 else -> {
initializeQQMusiceFunctions();
}else{
initializeMusiceFunctions()
}
}else -> {
Log.w(TAG, "未指定音乐服务类型,默认使用网易") 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) 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服务器 * 连接到SSE服务器

460
apps/client/local_plugins/chat_api/android/src/main/kotlin/com/yunqiinnovation/chat_api/QQMusicSystemFunctionHandler.kt

@ -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<String, Any>(),
"required" to emptyList<String>()
),
QQMusicPauseHandler(context)
)
client.registerLocalFunction(
"qqmusic_resume",
"恢复播放QQ音乐",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicResumeHandler(context)
)
client.registerLocalFunction(
"qqmusic_stop",
"停止QQ音乐播放",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicStopHandler(context)
)
client.registerLocalFunction(
"qqmusic_next",
"QQ音乐下一首",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
QQMusicNextHandler(context)
)
client.registerLocalFunction(
"qqmusic_previous",
"QQ音乐上一首",
mapOf(
"type" to "object",
"properties" to emptyMap<String, Any>(),
"required" to emptyList<String>()
),
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, Any>): 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<String, Any?>
// 添加调试日志
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, Any>): String {
return try {
@Suppress("UNCHECKED_CAST")
val songMids = arguments["songMids"] as? List<String> ?: return "{\"success\": false, \"message\": \"缺少歌曲ID列表\"}"
val index = (arguments["index"] as? Number)?.toInt() ?: 0
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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, Any>): String {
return try {
val qqMusicSingleton = QQMusicSingleton.getInstance()
var result = ""
// 创建一个CompletableDeferred用于等待回调结果
val deferred = CompletableDeferred<String>()
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}\"}"
}
}
}

2
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: "../../../swift_sdk_local"),
.package(path: "../../../location_service/ios/location_service"), .package(path: "../../../location_service/ios/location_service"),
.package(path: "../../../music_service/ios/music_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: "MCP", package: "swift_sdk_local"),
.product(name: "OpenAI", package: "OpenAI"), .product(name: "OpenAI", package: "OpenAI"),
.product(name: "music-service", package: "music_service"), .product(name: "music-service", package: "music_service"),
.product(name: "qq-music", package: "qq_music"),
.product(name: "location-service", package: "location_service") .product(name: "location-service", package: "location_service")
], ],
exclude: [] exclude: []

18
apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/MCPClient.swift

@ -535,18 +535,11 @@ public class MCPClient {
switch musicServiceType { switch musicServiceType {
case "WY": case "WY":
initializeMusicSystemFunction() // 更明确的命名 initializeMusicSystemFunction() // 更明确的命名
case "QQ":
initializeQQMusicSystemFunction()
default: default:
// 正确传入参数,并明确检查的是QQ音乐 // QQ 音乐已于 2026-09-24 下线,"QQ" 与未指定一律走网易/系统音乐
if QQMusicSystemFunctionHandler.isMusicAppInstalled() {
initializeQQMusicSystemFunction()
} else {
// 可以考虑是否需要检查网易云音乐是否安装
initializeMusicSystemFunction() initializeMusicSystemFunction()
} }
} }
}
private func initializeSystemFunctions() { private func initializeSystemFunctions() {
do { do {
@ -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 { public func connectToSSE(mcpConfigJson: String) async throws -> Bool {

332
apps/client/local_plugins/chat_api/ios/chat_api/Sources/chat_api/QQMusicSystemFunctionHandler.swift

@ -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) ?? "{}"
}
}

33
apps/client/local_plugins/qq_music/.gitignore

@ -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/

33
apps/client/local_plugins/qq_music/.metadata

@ -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'

3
apps/client/local_plugins/qq_music/CHANGELOG.md

@ -1,3 +0,0 @@
## 0.0.1
* TODO: Describe initial release.

1
apps/client/local_plugins/qq_music/LICENSE

@ -1 +0,0 @@
TODO: Add your license here.

15
apps/client/local_plugins/qq_music/README.md

@ -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.

4
apps/client/local_plugins/qq_music/analysis_options.yaml

@ -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

9
apps/client/local_plugins/qq_music/android/.gitignore

@ -1,9 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures
.cxx

79
apps/client/local_plugins/qq_music/android/build.gradle

@ -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')
}
}

BIN
apps/client/local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar

Binary file not shown.

1
apps/client/local_plugins/qq_music/android/settings.gradle

@ -1 +0,0 @@
rootProject.name = 'qq_music'

9
apps/client/local_plugins/qq_music/android/src/main/AndroidManifest.xml

@ -1,9 +0,0 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yunqiinnovation.qq_music">
<uses-permission android:name="android.permission.INTERNET" />
<application>
<!-- 移除了 LoginExampleActivity 的声明,因为该类已被删除 -->
</application>
</manifest>

15
apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl

@ -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);
}

8
apps/client/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl

@ -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);
}

53
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java

@ -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;
}

306
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt

@ -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)
}
}
}

726
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt

@ -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<Map<String, Any?>>)
fun onError(errorCode: Int, errorMessage: String)
}
interface PlayCallback {
fun onSuccess()
fun onError(errorCode: Int, errorMessage: String)
}
interface InfoCallback {
fun onSuccess(data: Map<String, Any?>)
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<Map<String, Any?>>()
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<String>,
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<String, Any?>()
// 处理歌曲信息
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"
}
}
}

690
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt

@ -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<String, Runnable>()
/**
* 初始化单例,设置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>) -> 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<LoginResult>) -> 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<List<Map<String, Any?>>>) -> Unit
) {
executeSearchbackAction("搜索歌曲",0,2) { musicManager, searchCallback ->
musicManager.searchSongs(keyword,searchType,searchCallback)
}(callback)
}
/**
* 播放歌曲列表(带自动登录检查)
*/
fun playSongs(
songMids: List<String>,
index: Int = 0,
callback: (Result<Unit>) -> Unit
) {
executePlaybackAction("播放列表",0,2) { musicManager, playCallback ->
Log.d(TAG, "播放歌曲:${songMids[index]}")
musicManager.playSongs(songMids, index,playCallback)
}(callback)
}
/**
* 播放音乐
*/
fun playMusic(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("播放",0,2) { musicManager, playCallback ->
musicManager.playMusic(playCallback)
}(callback)
}
/**
* 暂停音乐
*/
fun pauseMusic(callback: (Result<Unit>) -> 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>) -> 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>) -> Unit) {
executePlaybackAction("恢复播放",0,0) { musicManager, playCallback ->
musicManager.resumeMusic(playCallback)
}(callback)
}
/**
* 停止音乐
*/
fun stopMusic(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("停止",0,0) { musicManager, playCallback ->
musicManager.stopMusic(playCallback)
}(callback)
}
/**
* 下一首
*/
fun skipToNext(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("下一首",0,0) { musicManager, playCallback ->
musicManager.skipToNext(playCallback)
}(callback)
}
/**
* 上一首
*/
fun skipToPrevious(callback: (Result<Unit>) -> Unit) {
executePlaybackAction("上一首",0,0) { musicManager, playCallback ->
musicManager.skipToPrevious(playCallback)
}(callback)
}
/**
* 获取播放状态
*/
fun getPlaybackState(callback: (Result<Map<String, Any?>>) -> Unit) {
executeInfoAction("获取播放状态") { musicManager, infoCallback ->
musicManager.getPlaybackState(infoCallback)
}(callback)
}
/**
* 获取当前歌曲信息
*/
fun getCurrentSong(callback: (Result<Map<String, Any?>>) -> Unit) {
executeInfoAction("获取当前歌曲") { musicManager, infoCallback ->
musicManager.getCurrentSong(infoCallback)
}(callback)
}
/**
* 获取播放时间
*/
fun getPlayTime(callback: (Result<Map<String, Any?>>) -> Unit) {
executeInfoAction("获取播放时间") { musicManager, infoCallback ->
musicManager.getPlayTime(infoCallback)
}(callback)
}
/**
* 同步当前播放信息 - 统一接口
*/
fun syncCurrentPlayInfo(callback: (Result<Map<String, Any?>>) -> 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<List<Map<String, Any?>>>) -> 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<List<Map<String, Any?>>>) -> Unit
) {
Log.d(TAG, "开始 ${actionName}")
ensureMusicManager(context!!, object: QQMusicManager.BindCallback{
override fun onSuccess(){
action(musicManager!!, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) {
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) -> 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>) -> 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>) -> Unit
) {
musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
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<Map<String, Any?>>) -> 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<String, Any?>) {
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
)
}

237
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt

@ -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<String>("keyword")
val searchType = call.argument<Int>("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<List<String>>("songMids")
val index = call.argument<Int>("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()
}
}
}
}

64
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java

@ -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;
}
}

314
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java

@ -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<String, Object> 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<String, Object> 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<String, Object> keyMap) {
Key key = (Key) keyMap.get(PRIVATE_KEY);
return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
}
/**
* 获取公钥
*
* @param keyMap 密钥对
* @return
*/
public static String getPublicKey(Map<String, Object> keyMap) {
Key key = (Key) keyMap.get(PUBLIC_KEY);
return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
}
}

253
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt

@ -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)
}
}

403
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt

@ -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<Data.MediaInfo>(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<ParcelFileDescriptor>(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<Data.MediaInfo>(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<ParcelFileDescriptor>(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()
}

17
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java

@ -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;
}
}

218
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt

@ -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)
}
}

105
apps/client/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt

@ -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}]")
}
}
}
}

171
apps/client/local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml

@ -1,171 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillColor="#26A69A"
android:pathData="M0,0h108v108h-108z"/>
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeColor="#33FFFFFF"
android:strokeWidth="0.8"/>
</vector>

8
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tencent.qqmusic.api.demo.CallbackActivity">
</android.support.constraint.ConstraintLayout>

17
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ExampleListActivity">
<ListView
android:id="@+id/lv_examples"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>

20
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView
android:paddingVertical="15dp"
android:paddingHorizontal="15dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/login_hint" />
<Button
android:id="@+id/bt_bind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="login" />
</LinearLayout>

170
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_main.xml

@ -1,170 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.tencent.qqmusic.api.demo.MainActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
app:layout_constraintTop_toBottomOf="@id/appbar_layout">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/tv_connect_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="" />
<TextView
android:id="@+id/tv_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:text="" />
<TextView
android:id="@+id/tv_current_song"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="" />
<TextView
android:id="@+id/tv_song_list_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="songListSize:" />
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="action">
<AutoCompleteTextView
android:id="@+id/et_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:completionThreshold="0"
android:ems="10"
android:inputType="textPersonName"
android:text="hi" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="action params">
<EditText
android:id="@+id/et_params_key"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:hint="enter key here"
android:text="midList"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_action" />
</android.support.design.widget.TextInputLayout>
<android.support.design.widget.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="action params value">
<EditText
android:id="@+id/et_params_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:ems="10"
android:inputType="textMultiLine"
android:text="004Sw1Bp0tFS18" />
</android.support.design.widget.TextInputLayout>
<Button
android:id="@+id/bt_execute"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Execute(同步接口)" />
<Button
android:id="@+id/bt_execute_async"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:text="Execute Async(异步接口)" />
<Button
android:id="@+id/bt_register"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="register" />
<Button
android:id="@+id/bt_unregister"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="unregister" />
</LinearLayout>
</ScrollView>
</android.support.constraint.ConstraintLayout>

48
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_pcm_example.xml

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<android.support.v7.widget.AppCompatTextView
android:id="@+id/tv_information"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
/>
<Button
android:id="@+id/bt_bind_service"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="bind service" />
<Button
android:id="@+id/bt_start_pcm_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start pcm mode" />
<Button
android:id="@+id/bt_stop_pcm_mode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Stop pcm mode" />
<Button
android:id="@+id/bt_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Resume/Pause" />
<Button
android:id="@+id/bt_next"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Play next" />
<android.support.v7.widget.AppCompatTextView
android:text="@string/pcm_hint"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

220
apps/client/local_plugins/qq_music/android/src/main/res/layout/activity_visual.xml

@ -1,220 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".VisualActivity">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</android.support.design.widget.AppBarLayout>
<RelativeLayout
android:id="@+id/playerPanel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/appbar_layout"
android:layout_marginTop="5dp">
<ImageView
android:id="@+id/SongPic"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp" />
<TextView
android:id="@+id/txtPlayTime"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_alignParentRight="true"
android:layout_marginTop="5dp"
android:layout_marginRight="5dp"
android:text="时间" />
<TextView
android:id="@+id/txtSongInfos"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:layout_toRightOf="@+id/SongPic"
android:ems="10"
android:text="歌曲信息" />
<TextView
android:id="@+id/txtAlbum"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_below="@+id/txtSongInfos"
android:layout_marginLeft="5dp"
android:layout_marginTop="5dp"
android:layout_toRightOf="@+id/SongPic"
android:text="专辑-歌手" />
<LinearLayout
android:id="@+id/ll_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/SongPic"
android:orientation="horizontal">
<Button
android:id="@+id/btnActive"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="授权" />
<Button
android:id="@+id/btnPlayPre"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onPlayPre"
android:text="上一首" />
<Button
android:id="@+id/btnPlayPause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onPlayPause"
android:text="播放" />
<Button
android:id="@+id/btnPlayNext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onPlayNext"
android:text="下一首" />
<Button
android:id="@+id/btnLove"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:onClick="onLoveClick"
android:text="收藏" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_opi_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_button"
android:orientation="horizontal">
<Button
android:id="@+id/btnOpiSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClickOpiSearch"
android:text="Opi搜索" />
<Button
android:id="@+id/btnOpiMvTag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClickOpiMvTag"
android:text="Opi重命名" />
<Button
android:id="@+id/btnSchemeAction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="playSongMid"
android:text="自定义跳转" />
<Button
android:id="@+id/btnSearch"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClickSearch"
android:text="搜索" />
</LinearLayout>
<LinearLayout
android:id="@+id/ll_test_1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/ll_opi_button"
android:orientation="horizontal">
<Button
android:id="@+id/btnLyric"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="onClickLyric"
android:text="歌词" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="testGetFolderList"
android:text="快退1s" />
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="testPlayList"
android:text="快进1s" />
</LinearLayout>
<TextView
android:id="@+id/txtResult"
android:layout_width="fill_parent"
android:layout_height="35dp"
android:layout_below="@+id/ll_test_1"
android:ems="10"
android:text="Debug informations" />
<ProgressBar
android:id="@+id/ProgressPlay"
style="@android:style/Widget.ProgressBar.Horizontal"
android:layout_width="fill_parent"
android:layout_height="5dp"
android:layout_below="@+id/txtResult"
android:layout_marginTop="2dp"
android:max="1000"
android:progress="0"
android:secondaryProgress="0" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/listview_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/playerPanel">
<ListView
android:id="@+id/listview_folder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp" />
<ListView
android:id="@+id/listview_song"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:visibility="gone" />
</RelativeLayout>
</RelativeLayout>

21
apps/client/local_plugins/qq_music/android/src/main/res/layout/folder_list_view_item.xml

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="60dp"
android:orientation="horizontal">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="15dp"
android:layout_gravity="center_vertical"
android:text="标题"
android:textColor="#000"
android:textSize="20sp"/>
</LinearLayout>
</RelativeLayout>

45
apps/client/local_plugins/qq_music/android/src/main/res/layout/song_list_view_item.xml

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="55dp"
android:orientation="horizontal">
<ImageView
android:id="@+id/item_img"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="3dp"
android:layout_gravity="center_vertical"
android:src="@mipmap/mini_album_default"
/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:id="@+id/item_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textColor="#000"
android:textSize="20sp"/>
<TextView
android:id="@+id/item_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="内容"
android:textColor="#000"
android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>
</RelativeLayout>

24
apps/client/local_plugins/qq_music/android/src/main/res/layout/view_popview_more.xml

@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="5dp"
android:background="@color/colorPrimary"
android:orientation="vertical">
<View
android:layout_width="match_parent"
android:layout_height="2dp"
android:background="@color/colorPrimaryDark" />
<TextView
android:id="@+id/text_test_api"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_gravity="center"
android:gravity="center"
android:text="点我测试!"
android:textColor="@color/white"
android:textSize="15sp" />
</LinearLayout>

14
apps/client/local_plugins/qq_music/android/src/main/res/menu/toolbar_menu.xml

@ -1,14 +0,0 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/item_bind_platform"
android:title="设备类型:"
app:showAsAction="always"
/>
<item
android:id="@+id/item_goto_test_api"
android:title="点我测试!"
app:showAsAction="never" />
</menu>

BIN
apps/client/local_plugins/qq_music/android/src/main/res/mipmap-hdpi/mini_album_default.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

18
apps/client/local_plugins/qq_music/android/src/main/res/values/array.xml

@ -1,18 +0,0 @@
<resources>
<string-array name="bind_platform_array">
<item>手机</item>
<item>TV</item>
<item>Car</item>
</string-array>
<string-array name="sear_type">
<item>专辑</item>
<item>歌单</item>
<item>歌词</item>
<item>混合搜索</item>
<item>mv</item>
<item>歌手歌曲</item>
<item>歌手</item>
<item>歌曲</item>
<item>用户</item>
</string-array>
</resources>

7
apps/client/local_plugins/qq_music/android/src/main/res/values/colors.xml

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#3F51B5</color>
<color name="colorPrimaryDark">#303F9F</color>
<color name="colorAccent">#FF4081</color>
<color name="white">#ffffff</color>
</resources>

4
apps/client/local_plugins/qq_music/android/src/main/res/values/dimens.xml

@ -1,4 +0,0 @@
<resources>
<dimen name="fab_margin">16dp</dimen>
<dimen name="dimen_width">80dp</dimen>
</resources>

12
apps/client/local_plugins/qq_music/android/src/main/res/values/strings.xml

@ -1,12 +0,0 @@
<resources>
<string name="app_name">ApiDemoQQMusic</string>
<string name="title_activity_qrcode">QRCodeActivity</string>
<string name="login_hint">使用QQ音乐手机端完成登录获取Openid OpenToken的例子\n
由于同一scheme在CallbackActivity和LoginExampleActivity都需要使用,避免混乱,AndroidManifest.xml中
默认将LoginExampleActivity的intent-filter注释,完整体验LoginExampleActivity前将其反注释,并将CallbackActivity的intent-filter注释进行注释"</string>
<string name="pcm_hint">1. 完成如[API调用方式](https://developer.y.qq.com/qplayaidl.html#/overview?id=二、api调用方式)中提及到的与QQMusicApiService的绑定流程。
2. 调用`executeAsync("startPcmMode")`使QQ音乐进入Pcm模式,如QQ音乐正在播放中,会停止播放。Client在该方法中传入callback,等待QQ音乐解码Pcm数据时回调。
3. QQ音乐开始播放时,Client可主动调用`execute("playMusic")`,QQ音乐会开始解码并在上一步骤的callback中返回mediaInfo以及pcmFileDescriptor,mediaInfo为Data.MediaInfo类型,提供播放器初始化时使用的参数,pcmFileDescriptor为ParcelFileDescriptor类型,为Pcm数据传输用的文件描述符。
4. 读取pcmFileDescriptor对应的数据流数据进行播放。
5. 使用完毕时,调用`execute("stopPcmMode")`结束Pcm模式。QQ音乐会停止正在播放的音乐。</string>
</resources>

20
apps/client/local_plugins/qq_music/android/src/main/res/values/styles.xml

@ -1,20 +0,0 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar"/>
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light"/>
</resources>

38
apps/client/local_plugins/qq_music/ios/.gitignore

@ -1,38 +0,0 @@
.idea/
.vagrant/
.sconsign.dblite
.svn/
.DS_Store
*.swp
profile
DerivedData/
build/
GeneratedPluginRegistrant.h
GeneratedPluginRegistrant.m
.generated/
*.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
!default.pbxuser
!default.mode1v3
!default.mode2v3
!default.perspectivev3
xcuserdata
*.moved-aside
*.pyc
*sync/
Icon?
.tags*
/Flutter/Generated.xcconfig
/Flutter/ephemeral/
/Flutter/flutter_export_environment.sh

33
apps/client/local_plugins/qq_music/ios/qq_music.podspec

@ -1,33 +0,0 @@
#
# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
# Run `pod lib lint qq_music.podspec` to validate before publishing.
#
Pod::Spec.new do |s|
s.name = 'qq_music'
s.version = '0.0.1'
s.summary = 'A new Flutter project.'
s.description = <<-DESC
A new Flutter project.
DESC
s.homepage = 'http://example.com'
s.license = { :file => '../LICENSE' }
s.author = { 'Your Company' => 'email@example.com' }
s.source = { :path => '.' }
s.source_files = 'qq_music/Sources/qq_music/**/*'
s.public_header_files = 'qq_music/Sources/qq_music/include/**/*.h'
s.dependency 'Flutter'
s.platform = :ios, '12.0'
# Add QPlayAutoSDK framework
s.vendored_libraries = 'qq_music/Frameworks/QPlayAutoSDK/libQPlayAutoSDK.a'
s.source_files += 'qq_music/Frameworks/QPlayAutoSDK/Headers/include/**/*.h'
s.public_header_files += 'qq_music/Frameworks/QPlayAutoSDK/Headers/include/**/*.h'
s.xcconfig = {
'HEADER_SEARCH_PATHS' => '$(SRCROOT)/../qq_music/Frameworks/QPlayAutoSDK/Headers/include',
'LIBRARY_SEARCH_PATHS' => '$(SRCROOT)/../qq_music/Frameworks/QPlayAutoSDK'
}
# Flutter.framework does not contain a i386 slice.
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
s.swift_version = '5.0'
end

47
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/Info.plist

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>libQPlayAutoSDK_x86_64.a</string>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-x86_64-simulator</string>
<key>LibraryPath</key>
<string>libQPlayAutoSDK_x86_64.a</string>
<key>SupportedArchitectures</key>
<array>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>libQPlayAutoSDK_arm64.a</string>
<key>HeadersPath</key>
<string>Headers</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libQPlayAutoSDK_arm64.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

238
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/QPlayAutoDefine.h

@ -1,238 +0,0 @@
//
// QPlayAutoDefine.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/17.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#ifndef QPlayAutoDefine_h
#define QPlayAutoDefine_h
extern NSString *const kQPlayAutoItemRootID;
extern NSString *const kQPlayAutoArgument_Count;
extern NSString *const kQPlayAutoArgument_PageIndex;
extern NSString *const kQPlayAutoArgument_ParentID;
extern NSString *const kQPlayAutoArgument_Lists;
extern NSString *const kQPlayAutoArgument_PlayMode;
extern NSString *const kQPlayAutoArgument_IsFav;
extern NSString *const kQPlayAutoArgument_Position;
extern NSString *const kQPlayAutoArgument_Song;
extern NSString *const kQPlayAutoArgument_SongID;
extern NSString *const kQPlayAutoArgument_State;
extern NSString *const kQPlayAutoArgument_Error;
/*
QPlayAuto错误
*/
typedef NS_ENUM(NSUInteger, QPlayAutoError)
{
QPlayAuto_OK=0, //成功
QPlayAuto_QueryAutoFailed=100, //查询车机信息失败
QPlayAuto_QueryMobileFailed=101, //查询移动设备失败
QPlayAuto_QuerySongsFailed_WithoutID=102, //查询歌单数据失败,父 ID 不存在
QPlayAuto_QuerySongsFailed_NoNetwork=103, //查询歌单数据失败,移动设备网络不通,无法下载数据
QPlayAuto_QuerySongsFailed_Unknow=104, //查询歌单数据失败,原因未知
QPlayAuto_SongIDError=105, //歌曲 ID 不存在
QPlayAuto_ReadError=106, //读取数据错误
QPlayAuto_ParamError=107, //参数错误
QPlayAuto_SystemError=108, //系统调用错误
QPlayAuto_CopyRightError=109, //无法播放,没有版权
QPlayAuto_LoginError=110, //无法读取,没有登录
QPlayAuto_PcmRepeat=111, //PCM 请求重复
QPlayAuto_NoPermission=112, //没有权限
QPlayAuto_NeedBuy = 113, //无法播放(数字专辑)
QPlayAuto_NotSupportFormat=114, //无法播放(不支持格式)
QPlayAuto_NeedAuth=115, //需要授权
QPlayAuto_NeedVIP = 116, //无法播放(需要购买VIP)
QPlayAuto_TryListen = 117, //试听歌曲,购买听完整版
QPlayAuto_TrafficAlert = 118, //无法播放(流量弹窗阻断)
QPlayAuto_OnlyWifi = 119, //无法播放(仅Wifi弹窗阻断)
QPlayAuto_AlreadyConnected=120, //已经连接车机
QPlayAuto_SeekError = 121, //拖拽进度错误
QPlayAuto_NoListenRight = 122, //无法播放(无版权)
QPlayAuto_SongNotMatch=123, //歌曲不匹配
QPlayAuto_Disconnect=124, //连接已断开
};
/**
QPlayAuto连接状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoConnectState)
{
QPlayAutoConnectState_Connected = 0, //连接成功
QPlayAutoConnectState_Disconnect = 1, //断开连接
QPlayAutoConnectState_Failed = -1, //连接失败
QPlayAutoConnectState_Cancel = -2, //连接取消
};
/*
QPlayAutoListItem类型
*/
typedef NS_ENUM(NSUInteger, QPlayAutoListItemType)
{
QPlayAutoListItemType_Song = 1, //歌曲
QPlayAutoListItemType_Normal = 2, //普通目录
QPlayAutoListItemType_Radio = 3, //电台
QPlayAutoListItemType_Folder = 4, //歌单
QPlayAutoListItemType_Album = 5, //专辑
};
typedef NS_ENUM(NSUInteger, QPlayAutoSongListType)
{
QPlayAutoSongListType_Daily = 108, //每日30首
};
/**
QPlayAuto播放状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayState)
{
QPlayAutoPlayState_Stop=0, //停止
QPlayAutoPlayState_Playing=1, //播放
QPlayAutoPlayState_Pause=2 //暂停
};
typedef NS_ENUM(NSInteger, QPlayAutoAssenceMode)
{
QPlayAutoAssenceMode_Full = 0, // 默认整首播放
QPlayAutoAssenceMode_Part = 1, // 仅播放精华片段
};
/**
QPlayAuto播放模式
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayMode)
{
QPlayAutoPlayMode_SequenceCircle=0, //列表循环
QPlayAutoPlayMode_SingleCircle=1, //单曲循环
QPlayAutoPlayMode_RandomCircle=2 //随机播放
};
/**
QPlayAuto搜索类型
*/
typedef NS_ENUM(NSInteger, QPlayAutoSearchType)
{
QPlayAutoSearchType_Song = 0, //歌曲
QPlayAutoSearchType_Album = 2, //专辑
QPlayAutoSearchType_Folder = 3, //歌单
QPlayAutoSearchType_Composite = 100, //综合
};
typedef NS_ENUM(NSInteger, QPlayAutoVipState)
{
QPlayAutoVipState_Normal = 0,
QPlayAutoVipState_VIP = 1,
QPlayAutoVipState_SVIP = 2,
};
/**
第三方App信息
*/
@interface QPlayAutoAppInfo : NSObject<NSCoding>
@property (nonatomic,strong) NSString *deviceId; //AppId
@property (nonatomic,assign) NSInteger deviceType;
@property (nonatomic,strong) NSString *scheme; //跳转scheme
@property (nonatomic,strong) NSString *brand; //品牌
@property (nonatomic,strong) NSString *name; //名称
@property (nonatomic,strong) NSString *appId; //名称
@property (nonatomic,strong) NSString *secretKey; //私钥
@property (nonatomic,strong) NSString *bundleId; //bundleId
@property (nonatomic,assign) NSInteger qmCommandPort; //Q音接收命令端口
@property (nonatomic,strong) NSString *qmHost; //Q音接收命令IP
@property (nonatomic,strong) NSDate *lastConnectDate;//上次连接的日期
@end
@interface QPlayAutoSinger : NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Mid; //Mid
@property (nonatomic,strong) NSString *name; //名称
@end
/*
QPlayAuto列表项(歌单、电台、排行榜、歌曲、etc)
*/
@interface QPlayAutoListItem: NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Name; //名称
@property (nonatomic,strong) NSString *SubName; //副标题
@property (nonatomic,strong) QPlayAutoSinger *singer; //歌手
@property (nonatomic,strong) NSString *Album; //专辑
@property (nonatomic,strong) NSString *Mid;
@property (nonatomic,assign) QPlayAutoVipState vipState;
@property (nonatomic,assign) QPlayAutoListItemType Type;
@property (nonatomic,assign) NSInteger Duration; //时长
@property (nonatomic,strong) NSString *CoverUri; //封面Uri
@property (nonatomic,assign) NSInteger totalCount; //总数
@property (nonatomic,assign) BOOL isFav; //是否收藏
@property (nonatomic,assign) BOOL isOrigin; //是否原唱
@property (nonatomic,assign) BOOL isNoAudioSource; //无音源
@property (nonatomic,strong) NSMutableArray<QPlayAutoListItem*> *items; //子列表
@property (nonatomic,weak) QPlayAutoListItem *parentItem; //父节点
@property (nonatomic,assign) BOOL isTryListen; //是否试听歌曲
@property (nonatomic,readonly) BOOL isSong; //歌曲
@property (nonatomic,readonly) BOOL isFolder;//歌单
@property (nonatomic,readonly) BOOL isAlbum; //专辑
@property (nonatomic,readonly) BOOL isRadio; //电台
@property (nonatomic,readonly) BOOL isDirectory; //普通目录
@property (nonatomic,readonly) BOOL isVIP; //VIP歌曲
@property (nonatomic,readonly) BOOL isSVIP; //SVIP歌曲
- (instancetype)initWithDictionary:(NSDictionary*)dict;
- (instancetype)initSongWithId:(NSString *)identifier isFav:(BOOL)isFav;
- (instancetype)initWithId:(NSString *)identifier type:(QPlayAutoListItemType)type;
//是否是根节点
- (BOOL)isRoot;
//是否有更多
- (BOOL)hasMore;
- (QPlayAutoListItem*)findItemWithID:(NSString*)ID;
@end
/**
QPlayAuto请求回调
@param success 是否成功
@param dict 结果Dictionary
*/
typedef void (^QPlayAutoRequestFinishBlock)(BOOL success, NSDictionary *dict);
@interface QPlayAutoRequestBase : NSObject
/** 请求类型 */
@property (nonatomic, assign) int type;
@end
@interface QPlayAutoSentence : NSObject <NSCoding>
@property (nonatomic, assign) NSInteger startTime;
@property (nonatomic, assign) NSInteger duration;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, readonly) NSString *startTimeText;
@property (nonatomic, readonly) NSString *endTimeText;
@end
@interface QPlayAutoLyric : NSObject <NSCoding>
@property(nonatomic,assign) BOOL isTxtType;//歌词是不是纯文本的歌词,没有时间标记
@property(nonatomic,strong) NSString *songId;
@property(nonatomic,strong) NSString *songName;
@property(nonatomic,strong) NSString *singerName;
@property(nonatomic,strong) NSString *albumName;
@property(nonatomic,readonly) NSString *text;//所有歌词文本
@property(nonatomic,strong) NSArray<QPlayAutoSentence *> *sentences;
- (NSString *)sentenceAtTime:(NSTimeInterval)time;
@end
#endif /* QPlayAutoDefine_h */

188
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/QPlayAutoSDK.h

@ -1,188 +0,0 @@
//
// QPlayAutoSDK.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/5.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QPlayAutoDefine.h"
#define APP_DEVICE_TYPE (3)
NS_ASSUME_NONNULL_BEGIN
typedef void (^QPlayAutoResultItemsBlock)(NSInteger errorCode,NSArray<QPlayAutoListItem *> *_Nullable items);
typedef void (^QPlayAutoResultParentItemBlock)(NSInteger errorCode,QPlayAutoListItem *parentItem);
extern NSString const * QPlayAutoSDKVersion;
@protocol QPlayAutoSDKDelegate <NSObject>
//连接状态变化回调
- (void)onQPlayAutoConnectStateChanged:(QPlayAutoConnectState)newState;
//播放状态状态变化回调
- (void)onQPlayAutoPlayStateChanged:(QPlayAutoPlayState)playState song:(QPlayAutoListItem *)song position:(NSInteger)position;
//歌曲播放进度
- (void)onQPlayAutoPlayProgressChanged:(QPlayAutoListItem *)song progress:(NSTimeInterval)progress duration:(NSTimeInterval)duration;
//歌曲收藏状态变化
- (void)onSongFavoriteStateChange:(NSString*)songID isFavorite:(BOOL)isFavorite;
//播放状态事件变化
- (void)onPlayModeChange:(QPlayAutoPlayMode)playMode;
//定时关闭事件
- (void)onPlayPausedByTimeoff;
//登陆状态
- (void)onLoginStateDidChanged:(BOOL)isLoginOK;
@end
@interface QPlayAutoSDK : NSObject
/// 注册App
/// @param name 名称
/// @param brand 品牌
/// @param deviceId ?
/// @param scheme 授权成功后Q音跳转合作方的scheme
/// @param appId Q音分配的Id
/// @param secretKey 合作方私钥
/// @param bundleId 合作方包名
+ (void)registerAppWithName:(NSString *)name brand:(NSString *)brand deviceId:(NSString *)deviceId scheme:(NSString *)scheme appId:(NSString *)appId secretKey:(NSString *)secretKey bundleId:(NSString *)bundleId;
/// 播放状态
+ (QPlayAutoPlayMode)currentPlayMode;
/// 当前歌曲
+ (QPlayAutoListItem *_Nullable)currentSong;
/// 当前播放状态
+ (QPlayAutoPlayState)currentPlayState;
/// 当前播放进度
+ (NSTimeInterval)currentProgress;
/// QQ音乐是否登录
+ (BOOL)isLoginOK;
/// 检查QQ音乐是否已安装
+ (BOOL)isQQMusicInstalled;
/// 是否已连接
+ (BOOL)isConnected;
/// 连接中
+ (BOOL)isConnecting;
///QQ音乐设备信息
+ (NSDictionary *)deviceInfo;
+(NSString *)openId;
+(NSString *)openToken;
///是否可以处理回调url
+ (BOOL)canHandleOpenURL:(NSURL *)url;
///处理回调url
+ (BOOL)handleOpenURL:(NSURL *)url;
/// 回调代理
+ (void)setDelegate:(id<QPlayAutoSDKDelegate>) delegate;
/// 拉端连接QPlay
/// - Parameter forceLogin: 是否强制登录
+ (void)connectAndForceLogin:(BOOL)forceLogin;
/// 停止
+ (void)stop;
/// 二次热启动连接(需要联系Q音打开此功能才能使用)
/// - Parameters:
/// - timeout: 超时时间
/// - completion: 回调
+ (void)reconnectWithTimeout:(NSTimeInterval)timeout completion:(QPlayAutoRequestFinishBlock)completion;
/// 跳转QQ音乐登录
/// @param bundleId 合作方bundleId
/// @param callbackUrl 跳转合作方url
+ (BOOL)loginQQMusicWithBundleId:(NSString *)bundleId callbackUrl:(NSString *)callbackUrl;
/// 获取数据
/// @param parent 父
/// @param pageIndex 页码
/// @param pageSize 页大小
/// @param completion 回调
+ (void)getDataItemsFromParent:(QPlayAutoListItem *)parent pageIndex:(NSUInteger)pageIndex pageSize:(NSUInteger)pageSize completion:(QPlayAutoResultParentItemBlock)completion;
/// 设置播放模式
/// @param playMode 播放模式
/// @param block 回调
+ (NSInteger)setPlayMode:(QPlayAutoPlayMode)playMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 设置播放整首还是高潮
/// @param assencceMode 高潮还是整首
/// @param block 回调
+ (NSInteger)setAssenceMode:(QPlayAutoAssenceMode)assencceMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 收藏/取消收藏(歌曲id)
/// @param song 歌曲
/// @param isFavorite 收藏/取消收藏
/// @param completion 回调
+ (void)setFavoriteStateWithSong:(QPlayAutoListItem *)song isFavorite:(BOOL)isFavorite completion:(void (^)(NSInteger errorCode))completion;
/// 播放歌曲
/// @param songList 歌曲列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playAtIndex:(NSArray<QPlayAutoListItem*> *)songList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
/// 通过歌曲Mid列表播放歌曲
/// @param songMidList 歌曲Mid列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playSongMidAtIndex:(NSArray<NSString*> *)songMidList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
///播放下一首
+ (void)playerPlayNextWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放上一首
+ (void)playerPlayPrevWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放暂停
+ (void)playerPlayPause;
///播放恢复
+ (void)playerPlayResumeWithCompletion:(void (^)(NSInteger errorCode))completion;
///Seek到指定位置
+ (void)playerSeek:(NSInteger)position;
/// 搜索歌曲
/// @param keyword 关键词
/// @param type 搜索类型
/// @param firstPage 是否第一页
/// @param completion 回调
+ (NSInteger)search:(NSString *)keyword
type:(QPlayAutoSearchType)type
firstPage:(BOOL)firstPage
completion:(QPlayAutoResultItemsBlock)completion;
/// 相似歌曲
/// @param song 歌曲
/// @param completion 回调
+ (void)requestSimilarWithSong:(QPlayAutoListItem *)song completion:(QPlayAutoResultItemsBlock)completion;
/// 查询歌词
/// @param song 歌曲
/// @param completion 回调
+ (void)requestLyricWithSong:(QPlayAutoListItem *)song completion:(void (^)(NSInteger errorCode,QPlayAutoLyric *_Nullable lyric))completion;
/// 获取特定歌曲列表
/// @param type 列表类型
/// @param completion 回调
+ (void)requestSonglistWithType:(QPlayAutoSongListType)type completion:(QPlayAutoResultItemsBlock)completion;
@end
NS_ASSUME_NONNULL_END

8
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/Headers/module.modulemap

@ -1,8 +0,0 @@
module QPlayAutoSDK {
header "QPlayAutoSDK.h"
header "QPlayAutoDefine.h"
export *
link "QPlayAutoSDK"
}

BIN
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-arm64/libQPlayAutoSDK_arm64.a

Binary file not shown.

238
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/QPlayAutoDefine.h

@ -1,238 +0,0 @@
//
// QPlayAutoDefine.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/17.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#ifndef QPlayAutoDefine_h
#define QPlayAutoDefine_h
extern NSString *const kQPlayAutoItemRootID;
extern NSString *const kQPlayAutoArgument_Count;
extern NSString *const kQPlayAutoArgument_PageIndex;
extern NSString *const kQPlayAutoArgument_ParentID;
extern NSString *const kQPlayAutoArgument_Lists;
extern NSString *const kQPlayAutoArgument_PlayMode;
extern NSString *const kQPlayAutoArgument_IsFav;
extern NSString *const kQPlayAutoArgument_Position;
extern NSString *const kQPlayAutoArgument_Song;
extern NSString *const kQPlayAutoArgument_SongID;
extern NSString *const kQPlayAutoArgument_State;
extern NSString *const kQPlayAutoArgument_Error;
/*
QPlayAuto错误
*/
typedef NS_ENUM(NSUInteger, QPlayAutoError)
{
QPlayAuto_OK=0, //成功
QPlayAuto_QueryAutoFailed=100, //查询车机信息失败
QPlayAuto_QueryMobileFailed=101, //查询移动设备失败
QPlayAuto_QuerySongsFailed_WithoutID=102, //查询歌单数据失败,父 ID 不存在
QPlayAuto_QuerySongsFailed_NoNetwork=103, //查询歌单数据失败,移动设备网络不通,无法下载数据
QPlayAuto_QuerySongsFailed_Unknow=104, //查询歌单数据失败,原因未知
QPlayAuto_SongIDError=105, //歌曲 ID 不存在
QPlayAuto_ReadError=106, //读取数据错误
QPlayAuto_ParamError=107, //参数错误
QPlayAuto_SystemError=108, //系统调用错误
QPlayAuto_CopyRightError=109, //无法播放,没有版权
QPlayAuto_LoginError=110, //无法读取,没有登录
QPlayAuto_PcmRepeat=111, //PCM 请求重复
QPlayAuto_NoPermission=112, //没有权限
QPlayAuto_NeedBuy = 113, //无法播放(数字专辑)
QPlayAuto_NotSupportFormat=114, //无法播放(不支持格式)
QPlayAuto_NeedAuth=115, //需要授权
QPlayAuto_NeedVIP = 116, //无法播放(需要购买VIP)
QPlayAuto_TryListen = 117, //试听歌曲,购买听完整版
QPlayAuto_TrafficAlert = 118, //无法播放(流量弹窗阻断)
QPlayAuto_OnlyWifi = 119, //无法播放(仅Wifi弹窗阻断)
QPlayAuto_AlreadyConnected=120, //已经连接车机
QPlayAuto_SeekError = 121, //拖拽进度错误
QPlayAuto_NoListenRight = 122, //无法播放(无版权)
QPlayAuto_SongNotMatch=123, //歌曲不匹配
QPlayAuto_Disconnect=124, //连接已断开
};
/**
QPlayAuto连接状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoConnectState)
{
QPlayAutoConnectState_Connected = 0, //连接成功
QPlayAutoConnectState_Disconnect = 1, //断开连接
QPlayAutoConnectState_Failed = -1, //连接失败
QPlayAutoConnectState_Cancel = -2, //连接取消
};
/*
QPlayAutoListItem类型
*/
typedef NS_ENUM(NSUInteger, QPlayAutoListItemType)
{
QPlayAutoListItemType_Song = 1, //歌曲
QPlayAutoListItemType_Normal = 2, //普通目录
QPlayAutoListItemType_Radio = 3, //电台
QPlayAutoListItemType_Folder = 4, //歌单
QPlayAutoListItemType_Album = 5, //专辑
};
typedef NS_ENUM(NSUInteger, QPlayAutoSongListType)
{
QPlayAutoSongListType_Daily = 108, //每日30首
};
/**
QPlayAuto播放状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayState)
{
QPlayAutoPlayState_Stop=0, //停止
QPlayAutoPlayState_Playing=1, //播放
QPlayAutoPlayState_Pause=2 //暂停
};
typedef NS_ENUM(NSInteger, QPlayAutoAssenceMode)
{
QPlayAutoAssenceMode_Full = 0, // 默认整首播放
QPlayAutoAssenceMode_Part = 1, // 仅播放精华片段
};
/**
QPlayAuto播放模式
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayMode)
{
QPlayAutoPlayMode_SequenceCircle=0, //列表循环
QPlayAutoPlayMode_SingleCircle=1, //单曲循环
QPlayAutoPlayMode_RandomCircle=2 //随机播放
};
/**
QPlayAuto搜索类型
*/
typedef NS_ENUM(NSInteger, QPlayAutoSearchType)
{
QPlayAutoSearchType_Song = 0, //歌曲
QPlayAutoSearchType_Album = 2, //专辑
QPlayAutoSearchType_Folder = 3, //歌单
QPlayAutoSearchType_Composite = 100, //综合
};
typedef NS_ENUM(NSInteger, QPlayAutoVipState)
{
QPlayAutoVipState_Normal = 0,
QPlayAutoVipState_VIP = 1,
QPlayAutoVipState_SVIP = 2,
};
/**
第三方App信息
*/
@interface QPlayAutoAppInfo : NSObject<NSCoding>
@property (nonatomic,strong) NSString *deviceId; //AppId
@property (nonatomic,assign) NSInteger deviceType;
@property (nonatomic,strong) NSString *scheme; //跳转scheme
@property (nonatomic,strong) NSString *brand; //品牌
@property (nonatomic,strong) NSString *name; //名称
@property (nonatomic,strong) NSString *appId; //名称
@property (nonatomic,strong) NSString *secretKey; //私钥
@property (nonatomic,strong) NSString *bundleId; //bundleId
@property (nonatomic,assign) NSInteger qmCommandPort; //Q音接收命令端口
@property (nonatomic,strong) NSString *qmHost; //Q音接收命令IP
@property (nonatomic,strong) NSDate *lastConnectDate;//上次连接的日期
@end
@interface QPlayAutoSinger : NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Mid; //Mid
@property (nonatomic,strong) NSString *name; //名称
@end
/*
QPlayAuto列表项(歌单、电台、排行榜、歌曲、etc)
*/
@interface QPlayAutoListItem: NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Name; //名称
@property (nonatomic,strong) NSString *SubName; //副标题
@property (nonatomic,strong) QPlayAutoSinger *singer; //歌手
@property (nonatomic,strong) NSString *Album; //专辑
@property (nonatomic,strong) NSString *Mid;
@property (nonatomic,assign) QPlayAutoVipState vipState;
@property (nonatomic,assign) QPlayAutoListItemType Type;
@property (nonatomic,assign) NSInteger Duration; //时长
@property (nonatomic,strong) NSString *CoverUri; //封面Uri
@property (nonatomic,assign) NSInteger totalCount; //总数
@property (nonatomic,assign) BOOL isFav; //是否收藏
@property (nonatomic,assign) BOOL isOrigin; //是否原唱
@property (nonatomic,assign) BOOL isNoAudioSource; //无音源
@property (nonatomic,strong) NSMutableArray<QPlayAutoListItem*> *items; //子列表
@property (nonatomic,weak) QPlayAutoListItem *parentItem; //父节点
@property (nonatomic,assign) BOOL isTryListen; //是否试听歌曲
@property (nonatomic,readonly) BOOL isSong; //歌曲
@property (nonatomic,readonly) BOOL isFolder;//歌单
@property (nonatomic,readonly) BOOL isAlbum; //专辑
@property (nonatomic,readonly) BOOL isRadio; //电台
@property (nonatomic,readonly) BOOL isDirectory; //普通目录
@property (nonatomic,readonly) BOOL isVIP; //VIP歌曲
@property (nonatomic,readonly) BOOL isSVIP; //SVIP歌曲
- (instancetype)initWithDictionary:(NSDictionary*)dict;
- (instancetype)initSongWithId:(NSString *)identifier isFav:(BOOL)isFav;
- (instancetype)initWithId:(NSString *)identifier type:(QPlayAutoListItemType)type;
//是否是根节点
- (BOOL)isRoot;
//是否有更多
- (BOOL)hasMore;
- (QPlayAutoListItem*)findItemWithID:(NSString*)ID;
@end
/**
QPlayAuto请求回调
@param success 是否成功
@param dict 结果Dictionary
*/
typedef void (^QPlayAutoRequestFinishBlock)(BOOL success, NSDictionary *dict);
@interface QPlayAutoRequestBase : NSObject
/** 请求类型 */
@property (nonatomic, assign) int type;
@end
@interface QPlayAutoSentence : NSObject <NSCoding>
@property (nonatomic, assign) NSInteger startTime;
@property (nonatomic, assign) NSInteger duration;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, readonly) NSString *startTimeText;
@property (nonatomic, readonly) NSString *endTimeText;
@end
@interface QPlayAutoLyric : NSObject <NSCoding>
@property(nonatomic,assign) BOOL isTxtType;//歌词是不是纯文本的歌词,没有时间标记
@property(nonatomic,strong) NSString *songId;
@property(nonatomic,strong) NSString *songName;
@property(nonatomic,strong) NSString *singerName;
@property(nonatomic,strong) NSString *albumName;
@property(nonatomic,readonly) NSString *text;//所有歌词文本
@property(nonatomic,strong) NSArray<QPlayAutoSentence *> *sentences;
- (NSString *)sentenceAtTime:(NSTimeInterval)time;
@end
#endif /* QPlayAutoDefine_h */

188
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/QPlayAutoSDK.h

@ -1,188 +0,0 @@
//
// QPlayAutoSDK.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/5.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QPlayAutoDefine.h"
#define APP_DEVICE_TYPE (3)
NS_ASSUME_NONNULL_BEGIN
typedef void (^QPlayAutoResultItemsBlock)(NSInteger errorCode,NSArray<QPlayAutoListItem *> *_Nullable items);
typedef void (^QPlayAutoResultParentItemBlock)(NSInteger errorCode,QPlayAutoListItem *parentItem);
extern NSString const * QPlayAutoSDKVersion;
@protocol QPlayAutoSDKDelegate <NSObject>
//连接状态变化回调
- (void)onQPlayAutoConnectStateChanged:(QPlayAutoConnectState)newState;
//播放状态状态变化回调
- (void)onQPlayAutoPlayStateChanged:(QPlayAutoPlayState)playState song:(QPlayAutoListItem *)song position:(NSInteger)position;
//歌曲播放进度
- (void)onQPlayAutoPlayProgressChanged:(QPlayAutoListItem *)song progress:(NSTimeInterval)progress duration:(NSTimeInterval)duration;
//歌曲收藏状态变化
- (void)onSongFavoriteStateChange:(NSString*)songID isFavorite:(BOOL)isFavorite;
//播放状态事件变化
- (void)onPlayModeChange:(QPlayAutoPlayMode)playMode;
//定时关闭事件
- (void)onPlayPausedByTimeoff;
//登陆状态
- (void)onLoginStateDidChanged:(BOOL)isLoginOK;
@end
@interface QPlayAutoSDK : NSObject
/// 注册App
/// @param name 名称
/// @param brand 品牌
/// @param deviceId ?
/// @param scheme 授权成功后Q音跳转合作方的scheme
/// @param appId Q音分配的Id
/// @param secretKey 合作方私钥
/// @param bundleId 合作方包名
+ (void)registerAppWithName:(NSString *)name brand:(NSString *)brand deviceId:(NSString *)deviceId scheme:(NSString *)scheme appId:(NSString *)appId secretKey:(NSString *)secretKey bundleId:(NSString *)bundleId;
/// 播放状态
+ (QPlayAutoPlayMode)currentPlayMode;
/// 当前歌曲
+ (QPlayAutoListItem *_Nullable)currentSong;
/// 当前播放状态
+ (QPlayAutoPlayState)currentPlayState;
/// 当前播放进度
+ (NSTimeInterval)currentProgress;
/// QQ音乐是否登录
+ (BOOL)isLoginOK;
/// 检查QQ音乐是否已安装
+ (BOOL)isQQMusicInstalled;
/// 是否已连接
+ (BOOL)isConnected;
/// 连接中
+ (BOOL)isConnecting;
///QQ音乐设备信息
+ (NSDictionary *)deviceInfo;
+(NSString *)openId;
+(NSString *)openToken;
///是否可以处理回调url
+ (BOOL)canHandleOpenURL:(NSURL *)url;
///处理回调url
+ (BOOL)handleOpenURL:(NSURL *)url;
/// 回调代理
+ (void)setDelegate:(id<QPlayAutoSDKDelegate>) delegate;
/// 拉端连接QPlay
/// - Parameter forceLogin: 是否强制登录
+ (void)connectAndForceLogin:(BOOL)forceLogin;
/// 停止
+ (void)stop;
/// 二次热启动连接(需要联系Q音打开此功能才能使用)
/// - Parameters:
/// - timeout: 超时时间
/// - completion: 回调
+ (void)reconnectWithTimeout:(NSTimeInterval)timeout completion:(QPlayAutoRequestFinishBlock)completion;
/// 跳转QQ音乐登录
/// @param bundleId 合作方bundleId
/// @param callbackUrl 跳转合作方url
+ (BOOL)loginQQMusicWithBundleId:(NSString *)bundleId callbackUrl:(NSString *)callbackUrl;
/// 获取数据
/// @param parent 父
/// @param pageIndex 页码
/// @param pageSize 页大小
/// @param completion 回调
+ (void)getDataItemsFromParent:(QPlayAutoListItem *)parent pageIndex:(NSUInteger)pageIndex pageSize:(NSUInteger)pageSize completion:(QPlayAutoResultParentItemBlock)completion;
/// 设置播放模式
/// @param playMode 播放模式
/// @param block 回调
+ (NSInteger)setPlayMode:(QPlayAutoPlayMode)playMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 设置播放整首还是高潮
/// @param assencceMode 高潮还是整首
/// @param block 回调
+ (NSInteger)setAssenceMode:(QPlayAutoAssenceMode)assencceMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 收藏/取消收藏(歌曲id)
/// @param song 歌曲
/// @param isFavorite 收藏/取消收藏
/// @param completion 回调
+ (void)setFavoriteStateWithSong:(QPlayAutoListItem *)song isFavorite:(BOOL)isFavorite completion:(void (^)(NSInteger errorCode))completion;
/// 播放歌曲
/// @param songList 歌曲列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playAtIndex:(NSArray<QPlayAutoListItem*> *)songList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
/// 通过歌曲Mid列表播放歌曲
/// @param songMidList 歌曲Mid列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playSongMidAtIndex:(NSArray<NSString*> *)songMidList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
///播放下一首
+ (void)playerPlayNextWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放上一首
+ (void)playerPlayPrevWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放暂停
+ (void)playerPlayPause;
///播放恢复
+ (void)playerPlayResumeWithCompletion:(void (^)(NSInteger errorCode))completion;
///Seek到指定位置
+ (void)playerSeek:(NSInteger)position;
/// 搜索歌曲
/// @param keyword 关键词
/// @param type 搜索类型
/// @param firstPage 是否第一页
/// @param completion 回调
+ (NSInteger)search:(NSString *)keyword
type:(QPlayAutoSearchType)type
firstPage:(BOOL)firstPage
completion:(QPlayAutoResultItemsBlock)completion;
/// 相似歌曲
/// @param song 歌曲
/// @param completion 回调
+ (void)requestSimilarWithSong:(QPlayAutoListItem *)song completion:(QPlayAutoResultItemsBlock)completion;
/// 查询歌词
/// @param song 歌曲
/// @param completion 回调
+ (void)requestLyricWithSong:(QPlayAutoListItem *)song completion:(void (^)(NSInteger errorCode,QPlayAutoLyric *_Nullable lyric))completion;
/// 获取特定歌曲列表
/// @param type 列表类型
/// @param completion 回调
+ (void)requestSonglistWithType:(QPlayAutoSongListType)type completion:(QPlayAutoResultItemsBlock)completion;
@end
NS_ASSUME_NONNULL_END

8
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/Headers/module.modulemap

@ -1,8 +0,0 @@
module QPlayAutoSDK {
header "QPlayAutoSDK.h"
header "QPlayAutoDefine.h"
export *
link "QPlayAutoSDK"
}

BIN
apps/client/local_plugins/qq_music/ios/qq_music/Frameworks/QPlayAutoSDK.xcframework/ios-x86_64-simulator/libQPlayAutoSDK_x86_64.a

Binary file not shown.

26
apps/client/local_plugins/qq_music/ios/qq_music/Package.swift

@ -1,26 +0,0 @@
// swift-tools-version: 5.7
import PackageDescription
let package = Package(
name: "qq_music",
platforms: [
.iOS(.v11)
],
products: [
.library(
name: "qq-music",
targets: ["qq_music"]
),
],
targets: [
.target(
name: "qq_music",
dependencies: ["QPlayAutoSDK"],
path: "Sources/qq_music"
),
.binaryTarget(
name: "QPlayAutoSDK",
path: "Frameworks/QPlayAutoSDK.xcframework"
)
]
)

932
apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QPlayAutoManager.swift

@ -1,932 +0,0 @@
import Foundation
import UIKit
import QPlayAutoSDK
@objc public class QPlayAutoManager: NSObject {
@objc public static let shared = QPlayAutoManager()
private var delegates: [WeakDelegate] = []
private var isAppRegistered = false
private var loginCompletionHandlers: [(Bool) -> Void] = []
// 添加token存储相关常量
private static let STORED_OPEN_ID_KEY = "QQMusic_OpenID"
private static let STORED_OPEN_TOKEN_KEY = "QQMusic_OpenToken"
private static let STORED_LOGIN_TIME_KEY = "QQMusic_LoginTime"
private static let TOKEN_EXPIRE_TIME: TimeInterval = 30 * 24 * 60 * 60 // 30天过期
// QQ音乐配置常量
private static let OPENID_APPID = "2000000921"
private static let 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-----"
private override init() {
super.init()
QPlayAutoSDK.setDelegate(self)
// Register for app lifecycle notifications
NotificationCenter.default.addObserver(
self,
selector: #selector(appWillTerminate),
name: UIApplication.willTerminateNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(appDidEnterBackground),
name: UIApplication.didEnterBackgroundNotification,
object: nil
)
registerAppWithDefaultConfig()
// 启用自动登录
// tryAutoLoginWithStoredToken()
}
@objc private func appWillTerminate() {
stop()
}
@objc private func appDidEnterBackground() {
// Optionally stop SDK when app goes to background
// stop()
}
// MARK: - Token存储和自动登录
/// 尝试使用保存的token自动登录
private func tryAutoLoginWithStoredToken() {
guard let storedOpenId = UserDefaults.standard.string(forKey: Self.STORED_OPEN_ID_KEY),
let storedOpenToken = UserDefaults.standard.string(forKey: Self.STORED_OPEN_TOKEN_KEY),
let loginTime = UserDefaults.standard.object(forKey: Self.STORED_LOGIN_TIME_KEY) as? Date else {
print("QPlayAutoManager: 没有找到保存的token")
return
}
// 检查token是否过期
let currentTime = Date()
if currentTime.timeIntervalSince(loginTime) > Self.TOKEN_EXPIRE_TIME {
print("QPlayAutoManager: 保存的token已过期,清除本地存储")
clearStoredToken()
return
}
print("QPlayAutoManager: 找到有效的保存token,尝试自动登录")
print("QPlayAutoManager: OpenID: \(storedOpenId)")
print("QPlayAutoManager: OpenToken: \(storedOpenToken.prefix(20))...")
// 这里可以尝试使用保存的token进行连接
// 注意:QPlayAutoSDK可能需要重新连接来验证token有效性
connect(forceLogin: false)
}
/// 保存登录token
private func saveLoginToken() {
let openId = QPlayAutoSDK.openId()
let openToken = QPlayAutoSDK.openToken()
guard !openId.isEmpty, !openToken.isEmpty else {
print("QPlayAutoManager: 无法获取token信息")
return
}
UserDefaults.standard.set(openId, forKey: Self.STORED_OPEN_ID_KEY)
UserDefaults.standard.set(openToken, forKey: Self.STORED_OPEN_TOKEN_KEY)
UserDefaults.standard.set(Date(), forKey: Self.STORED_LOGIN_TIME_KEY)
UserDefaults.standard.synchronize()
print("QPlayAutoManager: Token已保存到本地存储")
print("QPlayAutoManager: OpenID: \(openId)")
print("QPlayAutoManager: OpenToken: \(openToken.prefix(20))...")
}
/// 清除保存的token
private func clearStoredToken() {
UserDefaults.standard.removeObject(forKey: Self.STORED_OPEN_ID_KEY)
UserDefaults.standard.removeObject(forKey: Self.STORED_OPEN_TOKEN_KEY)
UserDefaults.standard.removeObject(forKey: Self.STORED_LOGIN_TIME_KEY)
UserDefaults.standard.synchronize()
print("QPlayAutoManager: 已清除本地保存的token")
}
/// 获取保存的token信息
@objc public func getStoredTokenInfo() -> [String: Any]? {
guard let openId = UserDefaults.standard.string(forKey: Self.STORED_OPEN_ID_KEY),
let openToken = UserDefaults.standard.string(forKey: Self.STORED_OPEN_TOKEN_KEY),
let loginTime = UserDefaults.standard.object(forKey: Self.STORED_LOGIN_TIME_KEY) as? Date else {
return nil
}
return [
"openId": openId,
"openToken": openToken,
"loginTime": loginTime,
"isExpired": Date().timeIntervalSince(loginTime) > Self.TOKEN_EXPIRE_TIME
]
}
/// 手动清除token(用于登出)
@objc public func logout() {
clearStoredToken()
// _isLoginOK = false
QPlayAutoSDK.stop()
print("QPlayAutoManager: 用户已登出,token已清除")
}
private func registerAppWithDefaultConfig() {
guard !isAppRegistered else {
print("QPlayAutoManager: App already registered, skipping")
return
}
// 确保完全停止之前的实例,避免端口占用
QPlayAutoSDK.stop()
// 添加短暂延迟确保资源完全释放
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
let deviceId = UIDevice.current.identifierForVendor?.uuidString ?? "unknown_device"
let bundleId = Bundle.main.bundleIdentifier ?? "com.yimai.eaimar"
QPlayAutoSDK.registerApp(
withName: "Eaimar",
brand: "SaiTong",
deviceId: deviceId,
scheme: "eaimar://qqmusic",
appId: Self.OPENID_APPID,
secretKey: Self.OPENID_APP_PRIVATE_KEY,
bundleId: bundleId
)
self.isAppRegistered = true
print("QPlayAutoManager: App registered successfully")
}
}
// MARK: - Delegate Management
@objc public func addDelegate(_ delegate: QPlayAutoManagerDelegate) {
delegates.append(WeakDelegate(delegate))
cleanupDelegates()
}
@objc public func removeDelegate(_ delegate: QPlayAutoManagerDelegate) {
delegates.removeAll { $0.delegate === delegate }
}
private func cleanupDelegates() {
delegates.removeAll { $0.delegate == nil }
}
// MARK: - App Registration
@objc public func registerApp(
name: String,
brand: String,
deviceId: String,
scheme: String,
appId: String,
secretKey: String,
bundleId: String
) {
QPlayAutoSDK.registerApp(
withName: name,
brand: brand,
deviceId: deviceId,
scheme: scheme,
appId: appId,
secretKey: secretKey,
bundleId: bundleId
)
}
// MARK: - Connection Management
@objc public func connect(forceLogin: Bool = false) {
print("QPlayAutoManager: 🔗 开始连接,forceLogin: \(forceLogin)")
guard isAppRegistered else {
print("QPlayAutoManager: ❌ App未注册,先执行注册")
registerAppWithDefaultConfig()
return
}
print("QPlayAutoManager: App已注册,开始连接")
let startTime = Date()
print("QPlayAutoManager: 调用 QPlayAutoSDK.connectAndForceLogin(\(forceLogin))")
QPlayAutoSDK.connectAndForceLogin(forceLogin)
// 设置超时检查
let timeoutInterval: TimeInterval = forceLogin ? 45.0 : 10.0
DispatchQueue.main.asyncAfter(deadline: .now() + timeoutInterval) {
let endTime = Date()
let duration = endTime.timeIntervalSince(startTime)
print("QPlayAutoManager: 连接尝试完成,耗时: \(String(format: "%.2f", duration))秒")
print("QPlayAutoManager: 连接状态检查 - isConnected: \(self.isConnected)")
if !self.isConnected {
print("QPlayAutoManager: ❌ 连接超时失败")
print("QPlayAutoManager: 最终状态 - isConnected: \(self.isConnected), isLoginOK: \(self.isLoginOK)")
// 清理登录回调队列
if !self.loginCompletionHandlers.isEmpty {
print("QPlayAutoManager: 清理登录回调队列")
self.notifyLoginCompletion(false)
}
} else {
print("QPlayAutoManager: ✅ 连接成功")
if forceLogin {
print("QPlayAutoManager: 强制登录模式,等待用户授权...")
}
}
}
}
@objc public func reconnect(timeout: TimeInterval, completion: @escaping (Bool, [String: Any]?) -> Void) {
print("QPlayAutoManager: 开始执行reconnect,超时时间: \(timeout)秒")
print("QPlayAutoManager: 执行前状态检查:")
print(" - isConnected: \(QPlayAutoSDK.isConnected())")
print(" - isConnecting: \(QPlayAutoSDK.isConnecting())")
print(" - isLoginOK: \(QPlayAutoSDK.isLoginOK())")
print(" - isQQMusicInstalled: \(QPlayAutoSDK.isQQMusicInstalled())")
let startTime = Date()
QPlayAutoSDK.reconnect(withTimeout: timeout) { (success, dict) in
let endTime = Date()
let duration = endTime.timeIntervalSince(startTime)
print("QPlayAutoManager: reconnect完成,耗时: \(String(format: "%.2f", duration))秒")
print("QPlayAutoManager: reconnect结果: \(success ? "成功" : "失败")")
print("QPlayAutoManager: 返回数据: \(dict ?? [:])")
print("QPlayAutoManager: 执行后状态检查:")
print(" - isConnected: \(QPlayAutoSDK.isConnected())")
print(" - isConnecting: \(QPlayAutoSDK.isConnecting())")
print(" - isLoginOK: \(QPlayAutoSDK.isLoginOK())")
if !success {
// 分析可能的失败原因
if !QPlayAutoSDK.isQQMusicInstalled() {
print("QPlayAutoManager: ❌ 失败原因:QQ音乐未安装")
} else if !QPlayAutoSDK.isConnected() {
print("QPlayAutoManager: ❌ 失败原因:网络连接失败或服务不可用")
} else {
print("QPlayAutoManager: ❌ 失败原因:token验证失败或其他SDK内部错误")
}
}
completion(success, dict as? [String: Any])
}
}
@objc public func stop() {
QPlayAutoSDK.stop()
}
// MARK: - 登录状态管理
// Add the error enum at the top of the class
enum QPlayAutoError: Error {
case qqMusicNotInstalled
case loginFailed
case connectionFailed
case searchFailed(String) // 新增搜索失败错误
var localizedDescription: String {
switch self {
case .qqMusicNotInstalled:
return "QQ音乐未安装"
case .loginFailed:
return "登录失败"
case .connectionFailed:
return "连接失败"
case .searchFailed(let reason):
return "搜索失败: \(reason)"
}
}
}
private func ensureQQMusicInstalledAndLoginAndExecute(
_ operation: @escaping () -> Void,
onQQMusicNotInstalled: (() -> Void)? = nil,
onLoginFailed: (() -> Void)? = nil
) {
// 首先检查QQ音乐是否已安装
guard isQQMusicInstalled else {
print("QPlayAutoManager: QQ音乐未安装,无法执行操作")
onQQMusicNotInstalled?()
return
}
// QQ音乐已安装,继续检查登录状态
ensureLoginAndExecute(operation) { error in
switch error {
case .loginFailed:
onLoginFailed?()
case .qqMusicNotInstalled:
// This shouldn't happen since we already checked above
onQQMusicNotInstalled?()
default:
onLoginFailed?()
}
}
}
// 修改现有的ensureLoginAndExecute方法
private func ensureLoginAndExecute(
_ operation: @escaping () -> Void,
onFailure: ((QPlayAutoError) -> Void)? = nil
) {
// 首先检查QQ音乐是否已安装
guard isQQMusicInstalled else {
print("QPlayAutoManager: QQ音乐未安装,无法执行操作")
onFailure?(.qqMusicNotInstalled)
return
}
if isLoginOK {
// 已登录,检查连接状态
if isConnected {
// 已连接,直接执行操作
operation()
} else {
// 未连接,先连接
print("QPlayAutoManager: 未连接,尝试连接...")
connect(forceLogin: false)
// 超时检查
DispatchQueue.main.asyncAfter(deadline: .now() + 10.0) { [weak self] in
guard let self = self else { return }
if self.isConnected {
print("QPlayAutoManager: 连接成功,执行操作")
operation()
} else {
print("QPlayAutoManager: 连接超时失败,无法执行操作")
onFailure?(.connectionFailed)
}
}
}
} else {
// 未登录,先登录
print("QPlayAutoManager: 未登录,尝试登录...")
performLogin { [weak self] success in
guard let self = self else { return }
if success {
print("QPlayAutoManager: 登录成功,执行操作")
operation()
} else {
print("QPlayAutoManager: 登录失败,无法执行操作")
onFailure?(.loginFailed)
stop()
}
}
}
}
private func performLogin(completion: @escaping (Bool) -> Void) {
loginCompletionHandlers.append(completion)
if hasValidStoredToken() {
// 尝试热启动连接
reconnect(timeout: 10.0) { [weak self] success, responseDict in
if success {
self?.notifyLoginCompletion(true)
} else {
self?.forceLogin()
}
}
} else {
print("QPlayAutoManager: ❌ 没有有效token,直接进行强制登录")
forceLogin()
}
}
private func forceLogin() {
print("QPlayAutoManager: 🔄 开始强制登录流程")
print("QPlayAutoManager: 执行前环境检查:")
print(" - QQ音乐安装状态: \(isQQMusicInstalled)")
print(" - App注册状态: \(isAppRegistered)")
print(" - 当前连接状态: \(isConnected)")
print(" - 当前登录状态: \(isLoginOK)")
// 强制登录,会跳转到QQ音乐
connect(forceLogin: true)
// 设置超时监控(仅记录,不清理回调)
DispatchQueue.main.asyncAfter(deadline: .now() + 60.0) { [weak self] in
self?.logLoginTimeout()
}
}
private func hasValidStoredToken() -> Bool {
guard let storedOpenId = UserDefaults.standard.string(forKey: Self.STORED_OPEN_ID_KEY),
let storedOpenToken = UserDefaults.standard.string(forKey: Self.STORED_OPEN_TOKEN_KEY),
let loginTime = UserDefaults.standard.object(forKey: Self.STORED_LOGIN_TIME_KEY) as? Date else {
return false
}
// 检查token是否过期
let currentTime = Date()
return currentTime.timeIntervalSince(loginTime) <= Self.TOKEN_EXPIRE_TIME
}
private func logLoginTimeout() {
print("QPlayAutoManager: ⏰ 登录超时监控")
print("QPlayAutoManager: 当前状态 - 队列长度: \(loginCompletionHandlers.count), isLoginOK: \(isLoginOK)")
print("QPlayAutoManager: 连接状态 - isConnected: \(isConnected), isConnecting: \(isConnecting)")
if !loginCompletionHandlers.isEmpty && !isLoginOK {
print("QPlayAutoManager: ⚠️ 登录可能超时,但保留回调等待最终结果")
print("QPlayAutoManager: 如果确实超时,将由重试机制或连接失败回调处理")
} else if isLoginOK {
print("QPlayAutoManager: ✅ 登录已完成")
} else {
print("QPlayAutoManager: 无待处理的登录请求")
}
}
private func notifyLoginCompletion(_ success: Bool) {
let handlers = loginCompletionHandlers
loginCompletionHandlers.removeAll()
for handler in handlers {
handler(success)
}
}
// MARK: - 播放控制
@objc public func play() {
ensureQQMusicInstalledAndLoginAndExecute {
QPlayAutoSDK.playerPlayResume { errorCode in
print("Resume play result: \(errorCode)")
}
} onQQMusicNotInstalled: {
print("QPlayAutoManager: 播放失败 - QQ音乐未安装")
} onLoginFailed: {
print("QPlayAutoManager: 播放失败 - 登录失败")
}
}
@objc public func play(songId: String) {
ensureQQMusicInstalledAndLoginAndExecute {
// 使用正确的方法播放指定歌曲
QPlayAutoSDK.playSongMid(atIndex: [songId], play: 0) { errorCode in
print("Play song result: \(errorCode)")
}
} onQQMusicNotInstalled: {
print("QPlayAutoManager: 播放歌曲失败 - QQ音乐未安装")
} onLoginFailed: {
print("QPlayAutoManager: 播放歌曲失败 - 登录失败")
}
}
private var pendingPlayCompletion: ((Bool, String?) -> Void)?
private var playVerificationTimer: Timer?
@objc public func playSongs(mids: [String], index: Int = 0, completion: @escaping (Bool, String?) -> Void) {
playSongsWithRetry(mids: mids, index: index, retryCount: 0, maxRetries: 3, completion: completion)
}
private func playSongsWithRetry(mids: [String], index: Int, retryCount: Int, maxRetries: Int, completion: @escaping (Bool, String?) -> Void) {
guard !mids.isEmpty else {
completion(false, "歌曲列表为空")
return
}
guard index >= 0 && index < mids.count else {
completion(false, "播放索引超出范围")
return
}
ensureQQMusicInstalledAndLoginAndExecute {
QPlayAutoSDK.playSongMid(atIndex: mids, play: index) { errorCode in
if errorCode == 0 {
// 不立即返回成功,而是等待播放状态确认
self.pendingPlayCompletion = completion
// 设置超时检查
self.playVerificationTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in
if let pendingCompletion = self.pendingPlayCompletion {
self.pendingPlayCompletion = nil
// 检查当前播放状态
let currentState = self.currentPlayState
if currentState == .playing {
pendingCompletion(true, nil)
print("QQ音乐播放列表验证成功,共\(mids.count)首歌曲")
} else {
// 播放验证失败,尝试重试
if retryCount < maxRetries {
print("QQ音乐播放验证失败,第\(retryCount + 1)次重试...")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.playSongsWithRetry(mids: mids, index: index, retryCount: retryCount + 1, maxRetries: maxRetries, completion: completion)
}
} else {
pendingCompletion(false, "播放验证超时,已重试\(maxRetries)次,当前状态: \(currentState.rawValue)")
}
}
}
}
} else {
// SDK调用失败,尝试重试
if retryCount < maxRetries {
let errorMessage = "QQ音乐播放列表启动失败,错误码: \(errorCode),第\(retryCount + 1)次重试..."
print(errorMessage)
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
self.playSongsWithRetry(mids: mids, index: index, retryCount: retryCount + 1, maxRetries: maxRetries, completion: completion)
}
} else {
let errorMessage = "QQ音乐播放列表启动失败,已重试\(maxRetries)次,错误码: \(errorCode)"
completion(false, errorMessage)
print(errorMessage)
}
}
}
} onQQMusicNotInstalled: {
completion(false, "QQ音乐未安装,无法播放歌曲列表")
} onLoginFailed: {
completion(false, "登录失败,无法播放歌曲列表")
}
}
@objc public func next() {
ensureQQMusicInstalledAndLoginAndExecute {
QPlayAutoSDK.playerPlayNext { errorCode in
print("Next song result: \(errorCode)")
}
} onQQMusicNotInstalled: {
print("QPlayAutoManager: 下一首失败 - QQ音乐未安装")
} onLoginFailed: {
print("QPlayAutoManager: 下一首失败 - 登录失败")
}
}
@objc public func previous() {
ensureQQMusicInstalledAndLoginAndExecute {
QPlayAutoSDK.playerPlayPrev { errorCode in
print("Previous song result: \(errorCode)")
}
} onQQMusicNotInstalled: {
print("QPlayAutoManager: 上一首失败 - QQ音乐未安装")
} onLoginFailed: {
print("QPlayAutoManager: 上一首失败 - 登录失败")
}
}
@objc public func pause() {
ensureLoginAndExecute({
QPlayAutoSDK.playerPlayPause()
}, onFailure: { error in
print("QPlayAutoManager: 暂停失败 - \(error)")
})
}
// Fix seek method (lines 571-578)
@objc public func seek(to position: TimeInterval) {
ensureLoginAndExecute {
QPlayAutoSDK.playerSeek(Int(position))
} onFailure: { error in
print("QPlayAutoManager: 跳转播放位置失败 - \(error)")
}
}
// MARK: - 扩展功能方法(需要添加登录检查)
/// 设置播放模式
@objc public func setPlayMode(_ playMode: QPlayAutoPlayMode, completion: @escaping (Bool) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.setPlayMode(playMode) { (success, dict) in
completion(success)
}
} onFailure: { error in
print("QPlayAutoManager: 设置播放模式失败 - \(error)")
completion(false)
}
}
/// 收藏/取消收藏歌曲
@objc public func setFavoriteState(song: QPlayAutoListItem, isFavorite: Bool, completion: @escaping (Bool) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.setFavoriteStateWithSong(song, isFavorite: isFavorite) { errorCode in
completion(errorCode == 0)
}
}onFailure: { error in
print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)")
completion(false)
}
}
/// 获取相似歌曲
@objc public func requestSimilarSongs(song: QPlayAutoListItem, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.requestSimilar(withSong: song) { errorCode, items in
if errorCode == 0 {
completion(items, nil)
} else {
let error = NSError(domain: "QPlayAutoManager", code: Int(errorCode), userInfo: [NSLocalizedDescriptionKey: "请求相似歌曲失败"])
completion(nil, error)
}
}
}onFailure: { error in
print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)")
completion(nil, error)
}
}
/// 获取歌词
@objc public func requestLyric(song: QPlayAutoListItem, completion: @escaping (QPlayAutoLyric?, Error?) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.requestLyric(withSong: song) { errorCode, lyric in
if errorCode == 0 {
completion(lyric, nil)
} else {
let error = NSError(domain: "QPlayAutoManager", code: Int(errorCode), userInfo: [NSLocalizedDescriptionKey: "请求歌词失败"])
completion(nil, error)
}
}
} onFailure: { error in
print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)")
completion(nil, error)
}
}
/// 获取歌曲列表
@objc public func requestSonglist(type: QPlayAutoSongListType, completion: @escaping ([QPlayAutoListItem]?, Error?) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.requestSonglist(with: type) { errorCode, items in
if errorCode == 0 {
completion(items, nil)
} else {
let error = NSError(domain: "QPlayAutoManager", code: Int(errorCode), userInfo: [NSLocalizedDescriptionKey: "请求歌单失败"])
completion(nil, error)
}
}
} onFailure: { error in
print("QPlayAutoManager: 请求相似歌曲失败 - 登录失败 - \(error)")
completion(nil, error)
}
}
/// 播放指定歌曲列表
@objc public func playAtIndex(songList: [QPlayAutoListItem], playIndex: Int, completion: @escaping (Bool) -> Void) {
ensureLoginAndExecute {
QPlayAutoSDK.play(atIndex: songList, play: playIndex) { errorCode in
completion(errorCode == 0)
}
} onFailure: { error in
print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)")
completion(false)
}
}
// MARK: - 状态查询
@objc public var isConnected: Bool {
return QPlayAutoSDK.isConnected()
}
@objc public var isConnecting: Bool {
return QPlayAutoSDK.isConnecting()
}
@objc public var currentSong: QPlayAutoListItem? {
return QPlayAutoSDK.currentSong()
}
@objc public var currentPlayState: QPlayAutoPlayState {
return QPlayAutoSDK.currentPlayState()
}
@objc public var currentPlayMode: QPlayAutoPlayMode {
return QPlayAutoSDK.currentPlayMode()
}
@objc public var currentProgress: Int {
return Int(QPlayAutoSDK.currentProgress())
}
@objc public var isLoginOK: Bool {
return QPlayAutoSDK.isLoginOK()
}
@objc public var isQQMusicInstalled: Bool {
return QPlayAutoSDK.isQQMusicInstalled()
}
@objc public var deviceInfo: [String: Any] {
return QPlayAutoSDK.deviceInfo() as? [String: Any] ?? [:]
}
@objc public var openId: String? {
return QPlayAutoSDK.openId()
}
@objc public var openToken: String? {
return QPlayAutoSDK.openToken()
}
// MARK: - URL Handling
@objc public func canHandleOpenURL(url: String) -> Bool {
guard let nsurl = URL(string: url) else { return false }
// 检查QQ音乐是否安装
guard isQQMusicInstalled else {
print("QPlayAutoManager: QQ音乐未安装,无法处理URL")
return false
}
// 检查登录状态
guard isLoginOK else {
print("QPlayAutoManager: 未登录,无法处理URL")
return false
}
return QPlayAutoSDK.canHandleOpen(nsurl)
}
@objc public func handleOpenURL(_ url: URL) -> Bool {
// 检查QQ音乐是否安装
guard isQQMusicInstalled else {
print("QPlayAutoManager: QQ音乐未安装,无法处理URL")
return false
}
// 检查登录状态
guard isLoginOK else {
print("QPlayAutoManager: 未登录,无法处理URL")
return false
}
do {
return QPlayAutoSDK.handleOpen(url)
} catch {
print("QPlayAutoManager: 处理URL时发生错误: \(error)")
return false
}
}
// MARK: - Search and Content(添加登录检查)
@objc public func search(
keyword: String,
searchType: QPlayAutoSearchType,
completion: @escaping ([QPlayAutoListItem]?) -> Void,
onError: ((Error?) -> Void)? = nil
) {
ensureLoginAndExecute {
QPlayAutoSDK.search(keyword, type: searchType, firstPage: true) { (error, items) in
completion(items)
}
}onFailure: { error in
print("QPlayAutoManager: 搜索失败 - 登录失败 - \(error)")
let loginError = NSError(domain: "QPlayAutoManager", code: -1, userInfo: [
NSLocalizedDescriptionKey: "登录失败: \(error)"
])
onError?(loginError)
}
}
@objc public func getItems(
playlistId: String,
completion: @escaping ([QPlayAutoListItem]?) -> Void
) {
ensureLoginAndExecute {
let parentItem = QPlayAutoListItem()
parentItem.id = playlistId
QPlayAutoSDK.getDataItems(fromParent: parentItem, pageIndex: 0, pageSize: 50) { (error, items) in
if error == 0 {
completion(items as? [QPlayAutoListItem])
} else {
completion(nil)
}
}
} onFailure: { error in
print("QPlayAutoManager: 设置收藏状态失败 - 登录失败 - \(error)")
completion(nil)
}
}
// 添加公开的登录方法
@objc public func login(completion: @escaping (Bool) -> Void) {
performLogin(completion: completion)
}
}
// MARK: - QPlayAutoSDKDelegate
extension QPlayAutoManager: QPlayAutoSDKDelegate {
public func onQPlayAutoConnectStateChanged(_ newState: QPlayAutoConnectState) {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onConnectStateChanged?(newState)
}
}
public func onQPlayAutoPlayStateChanged(_ playState: QPlayAutoPlayState, song: QPlayAutoListItem, position: Int) {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onPlayStateChanged?(playState, song: song, position: position)
}
// 检查是否有待验证的播放请求
if let pendingCompletion = pendingPlayCompletion {
if playState == .playing {
pendingPlayCompletion = nil
playVerificationTimer?.invalidate()
pendingCompletion(true, nil)
print("QQ音乐播放状态确认:真正开始播放")
} else {
pendingPlayCompletion = nil
playVerificationTimer?.invalidate()
pendingCompletion(false, "播放失败,歌曲可能不可用")
}
}
}
public func onQPlayAutoPlayProgressChanged(_ song: QPlayAutoListItem, progress: TimeInterval, duration: TimeInterval) {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onPlayProgressChanged?(song, progress: progress, duration: duration)
}
}
public func onSongFavoriteStateChange(_ songID: String, isFavorite: Bool) {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onSongFavoriteStateChange?(songID, isFavorite: isFavorite)
}
}
public func onPlayModeChange(_ playMode: QPlayAutoPlayMode) {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onPlayModeChange?(playMode)
}
}
public func onPlayPausedByTimeoff() {
cleanupDelegates()
for weakDelegate in delegates {
weakDelegate.delegate?.onPlayPausedByTimeoff?()
}
}
public func onLoginStateDidChanged(_ isLoginOK: Bool) {
print("QPlayAutoManager: Login state changed to \(isLoginOK)")
// _isLoginOK = isLoginOK
// 登录成功时保存token并通知回调
if isLoginOK {
saveLoginToken()
print("QPlayAutoManager: ✅ 登录成功,通知所有等待的回调")
notifyLoginCompletion(true)
} else {
// 登录失败时清除可能无效的token
clearStoredToken()
print("QPlayAutoManager: ❌ 登录失败")
// 如果有等待的回调,立即通知失败
if !loginCompletionHandlers.isEmpty {
print("QPlayAutoManager: 登录失败,清理回调队列")
notifyLoginCompletion(false)
}
}
// 通知所有代理
cleanupDelegates()
delegates.forEach { weakDelegate in
weakDelegate.delegate?.onLoginStateDidChanged?(isLoginOK)
}
}
}
// MARK: - Delegate Protocol
@objc public protocol QPlayAutoManagerDelegate: AnyObject {
@objc optional func onConnectStateChanged(_ newState: QPlayAutoConnectState)
@objc optional func onPlayStateChanged(_ playState: QPlayAutoPlayState, song: QPlayAutoListItem, position: Int)
@objc optional func onPlayProgressChanged(_ song: QPlayAutoListItem, progress: TimeInterval, duration: TimeInterval)
@objc optional func onSongFavoriteStateChange(_ songID: String, isFavorite: Bool)
@objc optional func onPlayModeChange(_ playMode: QPlayAutoPlayMode)
@objc optional func onPlayPausedByTimeoff()
@objc optional func onLoginStateDidChanged(_ isLoginOK: Bool)
}
// MARK: - Helper Classes
private class WeakDelegate {
weak var delegate: QPlayAutoManagerDelegate?
init(_ delegate: QPlayAutoManagerDelegate) {
self.delegate = delegate
}
}

536
apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/QqMusicPlugin.swift

@ -1,536 +0,0 @@
import Flutter
import UIKit
import QPlayAutoSDK
public class QqMusicPlugin: NSObject, FlutterPlugin {
private var methodChannel: FlutterMethodChannel?
private var eventSink: FlutterEventSink?
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "com.yunqiinnovation.qq_music", binaryMessenger: registrar.messenger())
let instance = QqMusicPlugin()
instance.methodChannel = channel
registrar.addMethodCallDelegate(instance, channel: channel)
// 设置代理
QPlayAutoManager.shared.addDelegate(instance)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "registerApp":
handleRegisterApp(call, result: result)
case "connect":
handleConnect(call, result: result)
case "reconnect":
handleReconnect(call, result: result)
case "stop":
handleStop(call, result: result)
case "login":
handleLogin(call, result: result)
case "searchSongs":
handleSearchSongs(call, result: result)
case "playSongs":
handlePlaySongs(call, result: result)
case "play":
handlePlay(call, result: result)
case "playMusic":
handlePlayMusic(call, result: result)
case "pause":
handlePause(call, result: result)
case "pauseMusic":
handlePauseMusic(call, result: result)
case "resumeMusic":
handleResumeMusic(call, result: result)
case "stopMusic":
handleStopMusic(call, result: result)
case "next":
handleNext(call, result: result)
case "skipToNext":
handleSkipToNext(call, result: result)
case "previous":
handlePrevious(call, result: result)
case "skipToPrevious":
handleSkipToPrevious(call, result: result)
case "seek":
handleSeek(call, result: result)
case "isConnected":
result(QPlayAutoManager.shared.isConnected)
case "isConnecting":
result(QPlayAutoManager.shared.isConnecting)
case "currentSong":
handleCurrentSong(result: result)
case "getCurrentSong":
handleGetCurrentSong(result: result)
case "currentPlayState":
result(QPlayAutoManager.shared.currentPlayState.rawValue)
case "getPlaybackState":
handleGetPlaybackState(result: result)
case "currentPlayMode":
result(QPlayAutoManager.shared.currentPlayMode.rawValue)
case "currentProgress":
result(QPlayAutoManager.shared.currentProgress)
case "getPlayTime":
handleGetPlayTime(result: result)
case "isLoginOK":
result(QPlayAutoManager.shared.isLoginOK)
case "isQQMusicInstalled":
result(QPlayAutoManager.shared.isQQMusicInstalled)
case "openUrl":
handleOpenUrl(call, result: result)
case "search":
handleSearch(call, result: result)
case "getPlaylistContent":
handleGetPlaylistContent(call, result: result)
case "syncCurrentPlayInfo":
handleSyncCurrentPlayInfo(result: result)
case "getPlatformVersion":
handleGetPlatformVersion(result: result)
default:
result(FlutterMethodNotImplemented)
}
}
// MARK: - Method Handlers
private func handleRegisterApp(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let name = args["name"] as? String,
let brand = args["brand"] as? String,
let deviceId = args["deviceId"] as? String,
let scheme = args["scheme"] as? String,
let appId = args["appId"] as? String,
let secretKey = args["secretKey"] as? String,
let bundleId = args["bundleId"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing required parameters", details: nil))
return
}
QPlayAutoManager.shared.registerApp(
name: name,
brand: brand,
deviceId: deviceId,
scheme: scheme,
appId: appId,
secretKey: secretKey,
bundleId: bundleId
)
result(["success": true])
}
private func handleConnect(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
let forceLogin = (call.arguments as? [String: Any])?["forceLogin"] as? Bool ?? false
QPlayAutoManager.shared.connect(forceLogin: forceLogin)
result(["success": true])
}
private func handleReconnect(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let timeout = args["timeout"] as? Double else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing timeout", details: nil))
return
}
QPlayAutoManager.shared.reconnect(timeout: timeout) { success, dict in
result(["success": success, "data": dict ?? [:]])
}
}
private func handleStop(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.stop()
result(["success": true])
}
// 新增:自动登陆
private func handleLogin(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
//iOS 暂时不支持自动登陆
result([
"success": false,
])
return
// if QPlayAutoManager.shared.isLoginOK {
// let openId = QPlayAutoManager.shared.openId ?? ""
// let openToken = QPlayAutoManager.shared.openToken ?? ""
// result([
// "success": true,
// "openId": openId,
// "openToken": openToken,
// "expireTime": ""
// ])
// return
// }
//
// // 如果未登录,主动执行登录
// QPlayAutoManager.shared.login { success in
// DispatchQueue.main.async {
// if success {
// let openId = QPlayAutoManager.shared.openId ?? ""
// let openToken = QPlayAutoManager.shared.openToken ?? ""
// result([
// "success": true,
// "openId": openId,
// "openToken": openToken,
// "expireTime": ""
// ])
// } else {
// result(FlutterError(code: "LOGIN_FAILED", message: "登录失败", details: nil))
// }
// }
// }
}
// 新增:搜索歌曲功能
private func handleSearchSongs(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let keyword = args["keyword"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing keyword", details: nil))
return
}
let searchType = args["searchType"] as? Int ?? 0
print("开始搜索歌曲,关键词: \(keyword), 类型: \(searchType)")
QPlayAutoManager.shared.search(keyword: keyword, searchType: QPlayAutoSearchType(rawValue: searchType) ?? .song) { items in
DispatchQueue.main.async {
if let items = items {
print("搜索成功,找到 \(items.count) 首歌曲")
let songs = items.map { item -> [String: Any] in
return [
"id": item.id ?? "",
"name": item.name ?? "",
"artist": item.singer?.name ?? "",
"album": item.album ?? "",
"duration": item.duration,
"mid": item.id ?? ""
]
}
result(["success": true, "songs": songs])
} else {
print("搜索失败,未找到歌曲")
result(FlutterError(code: "SEARCH_FAILED", message: "Search failed", details: nil))
}
}
}
}
// 新增:播放歌曲列表功能
private func handlePlaySongs(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let songMids = args["songMids"] as? [String],
!songMids.isEmpty else {
result(FlutterError(code: "INVALID_ARGUMENT", message: "songMids is required", details: nil))
return
}
let index = args["index"] as? Int ?? 0
// 使用 playSongs 方法播放整个列表
QPlayAutoManager.shared.playSongs(mids: songMids, index: index) { success, errorMessage in
if success {
result(["success": true])
} else {
result(FlutterError(code: "PLAY_FAILED", message: errorMessage ?? "播放失败", details: nil))
}
}
}
private func handlePlay(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if let args = call.arguments as? [String: Any],
let songId = args["songId"] as? String {
QPlayAutoManager.shared.play(songId: songId)
} else {
QPlayAutoManager.shared.play()
}
result(["success": true])
}
private func handlePlayMusic(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.play()
result(["success": true])
}
private func handlePause(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.pause()
result(["success": true])
}
private func handlePauseMusic(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.pause()
result(["success": true])
}
private func handleResumeMusic(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.play()
result(["success": true])
}
private func handleStopMusic(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.stop()
result(["success": true])
}
private func handleNext(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.next()
result(["success": true])
}
private func handleSkipToNext(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.next()
result(["success": true])
}
private func handlePrevious(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.previous()
result(["success": true])
}
private func handleSkipToPrevious(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
QPlayAutoManager.shared.previous()
result(["success": true])
}
private func handleSeek(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let position = args["position"] as? Int else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing position", details: nil))
return
}
QPlayAutoManager.shared.seek(to: TimeInterval(position))
result(["success": true])
}
// 第286-290行 - handleCurrentSong方法
private func handleCurrentSong(result: @escaping FlutterResult) {
if let song = QPlayAutoManager.shared.currentSong {
let songInfo: [String: Any] = [
"id": song.id ?? "",
"name": song.name ?? "",
"artist": song.singer?.name ?? "",
"album": song.album ?? "",
"duration": song.duration
]
result(songInfo)
} else {
result(nil)
}
}
// 第302-307行 - handleGetCurrentSong方法
private func handleGetCurrentSong(result: @escaping FlutterResult) {
if let song = QPlayAutoManager.shared.currentSong {
let songInfo: [String: Any] = [
"id": song.id ?? "",
"name": song.name ?? "",
"artist": song.singer?.name ?? "",
"album": song.album ?? "",
"duration": song.duration,
"mid": song.id ?? ""
]
result(["success": true, "data": songInfo])
} else {
result(FlutterError(code: "NO_CURRENT_SONG", message: "No current song", details: nil))
}
}
// 第323行 - handleGetPlayTime方法
private func handleGetPlayTime(result: @escaping FlutterResult) {
if let currentSong = QPlayAutoManager.shared.currentSong {
let playTime: [String: Any] = [
"currentTime": QPlayAutoManager.shared.currentProgress,
"duration": currentSong.duration
]
result(playTime)
} else {
result(nil)
}
}
private func handleGetPlaybackState(result: @escaping FlutterResult) {
let playbackState: [String: Any] = [
"state": QPlayAutoManager.shared.currentPlayState.rawValue,
"position": QPlayAutoManager.shared.currentProgress
]
result(["success": true, "data": playbackState])
}
private func handleOpenUrl(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let urlString = args["url"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing url parameter", details: nil))
return
}
if QPlayAutoManager.shared.canHandleOpenURL(url: urlString),
let url = URL(string: urlString) {
let handled = QPlayAutoManager.shared.handleOpenURL(url)
result(["success": handled])
} else {
result(["success": false])
}
}
// 第357-361行 - handleSearch方法中的映射
private func handleSearch(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let keyword = args["keyword"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing keyword", details: nil))
return
}
let searchType = args["searchType"] as? Int ?? 0
QPlayAutoManager.shared.search(keyword: keyword, searchType: QPlayAutoSearchType(rawValue: searchType) ?? .song) { items in
if let items = items {
let songs = items.map { item -> [String: Any] in
return [
"id": item.id ?? "",
"name": item.name ?? "",
"artist": item.singer?.name ?? "",
"album": item.album ?? "",
"duration": item.duration
]
}
result(["success": true, "songs": songs])
} else {
result(["success": false, "songs": []])
}
}
}
// 第382-386行 - handleGetPlaylistContent方法中的映射
private func handleGetPlaylistContent(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? [String: Any],
let playlistId = args["playlistId"] as? String else {
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Missing playlistId", details: nil))
return
}
QPlayAutoManager.shared.getItems(playlistId: playlistId) { items in
if let items = items {
let songs = items.map { item -> [String: Any] in
return [
"id": item.id ?? "",
"name": item.name ?? "",
"artist": item.singer?.name ?? "",
"album": item.album ?? "",
"duration": item.duration
]
}
result(["success": true, "songs": songs])
} else {
result(["success": false, "songs": []])
}
}
}
// 第405-409行 - handleSyncCurrentPlayInfo方法
private func handleSyncCurrentPlayInfo(result: @escaping FlutterResult) {
// 构建同步数据
var syncData: [String: Any] = [:]
// 获取当前歌曲信息
if let song = QPlayAutoManager.shared.currentSong {
let currentSong: [String: Any] = [
"id": song.id ?? "",
"mid": song.mid ?? "",
"name": song.name ?? "",
"singer": song.singer?.name ?? "",
"album": song.album ?? "",
"duration": song.duration,
"pic": song.coverUri ?? ""
]
syncData["currentSong"] = currentSong
} else {
syncData["currentSong"] = nil
}
// 获取播放状态 - 使用正确的枚举成员名称
let playState = QPlayAutoManager.shared.currentPlayState
let isPlaying: Bool
let playStateValue: Int
switch playState {
case .playing:
isPlaying = true
playStateValue = 1
case .pause:
isPlaying = false
playStateValue = 2
case .stop:
isPlaying = false
playStateValue = 0
default:
isPlaying = false
playStateValue = 0
}
syncData["isPlaying"] = isPlaying
syncData["playState"] = playStateValue
// 获取播放时间
let currentProgress = QPlayAutoManager.shared.currentProgress
let currentTime = Int64(currentProgress) // 转换为秒
let totalTime = Int64(QPlayAutoManager.shared.currentSong?.duration ?? 0)
syncData["currentTime"] = currentTime
syncData["totalTime"] = totalTime
// 添加同步时间戳
syncData["syncTimestamp"] = Int64(Date().timeIntervalSince1970 * 1000) // 毫秒时间戳
result(["success": true, "data": syncData])
}
// 第492-496行 - 重复的handleGetPlaylistContent方法(需要删除)
// 删除这个重复的方法声明
// 第509-513行 - 重复的handleSyncCurrentPlayInfo方法(需要删除)
// 删除这个重复的方法声明
private func handleGetPlatformVersion(result: @escaping FlutterResult) {
let version = "iOS " + UIDevice.current.systemVersion
result(version)
}
}
// MARK: - FlutterStreamHandler
extension QqMusicPlugin: FlutterStreamHandler {
public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
// MARK: - QPlayAutoManagerDelegate
extension QqMusicPlugin: QPlayAutoManagerDelegate {
public func onConnectStateChanged(_ state: QPlayAutoConnectState) {
eventSink?(["type": "connectStateChanged", "state": state.rawValue])
}
public func onPlayStateChanged(_ state: QPlayAutoPlayState, song: QPlayAutoListItem, position: Int) {
eventSink?(["type": "playStateChanged", "state": state.rawValue])
}
public func onPlayProgressChanged(_ song: QPlayAutoListItem, progress: TimeInterval, duration: TimeInterval) {
eventSink?(["type": "progressChanged", "progress": Int(progress)])
}
public func onSongFavoriteStateChange(_ songID: String, isFavorite: Bool) {
eventSink?(["type": "favoriteStateChanged", "songId": songID, "isFavorite": isFavorite])
}
public func onPlayModeChange(_ mode: QPlayAutoPlayMode) {
eventSink?(["type": "playModeChanged", "mode": mode.rawValue])
}
public func onLoginStateDidChanged(_ isLogin: Bool) {
eventSink?(["type": "loginStateChanged", "isLogin": isLogin])
}
}

238
apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/QPlayAutoDefine.h

@ -1,238 +0,0 @@
//
// QPlayAutoDefine.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/17.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#ifndef QPlayAutoDefine_h
#define QPlayAutoDefine_h
extern NSString *const kQPlayAutoItemRootID;
extern NSString *const kQPlayAutoArgument_Count;
extern NSString *const kQPlayAutoArgument_PageIndex;
extern NSString *const kQPlayAutoArgument_ParentID;
extern NSString *const kQPlayAutoArgument_Lists;
extern NSString *const kQPlayAutoArgument_PlayMode;
extern NSString *const kQPlayAutoArgument_IsFav;
extern NSString *const kQPlayAutoArgument_Position;
extern NSString *const kQPlayAutoArgument_Song;
extern NSString *const kQPlayAutoArgument_SongID;
extern NSString *const kQPlayAutoArgument_State;
extern NSString *const kQPlayAutoArgument_Error;
/*
QPlayAuto错误
*/
typedef NS_ENUM(NSUInteger, QPlayAutoError)
{
QPlayAuto_OK=0, //成功
QPlayAuto_QueryAutoFailed=100, //查询车机信息失败
QPlayAuto_QueryMobileFailed=101, //查询移动设备失败
QPlayAuto_QuerySongsFailed_WithoutID=102, //查询歌单数据失败,父 ID 不存在
QPlayAuto_QuerySongsFailed_NoNetwork=103, //查询歌单数据失败,移动设备网络不通,无法下载数据
QPlayAuto_QuerySongsFailed_Unknow=104, //查询歌单数据失败,原因未知
QPlayAuto_SongIDError=105, //歌曲 ID 不存在
QPlayAuto_ReadError=106, //读取数据错误
QPlayAuto_ParamError=107, //参数错误
QPlayAuto_SystemError=108, //系统调用错误
QPlayAuto_CopyRightError=109, //无法播放,没有版权
QPlayAuto_LoginError=110, //无法读取,没有登录
QPlayAuto_PcmRepeat=111, //PCM 请求重复
QPlayAuto_NoPermission=112, //没有权限
QPlayAuto_NeedBuy = 113, //无法播放(数字专辑)
QPlayAuto_NotSupportFormat=114, //无法播放(不支持格式)
QPlayAuto_NeedAuth=115, //需要授权
QPlayAuto_NeedVIP = 116, //无法播放(需要购买VIP)
QPlayAuto_TryListen = 117, //试听歌曲,购买听完整版
QPlayAuto_TrafficAlert = 118, //无法播放(流量弹窗阻断)
QPlayAuto_OnlyWifi = 119, //无法播放(仅Wifi弹窗阻断)
QPlayAuto_AlreadyConnected=120, //已经连接车机
QPlayAuto_SeekError = 121, //拖拽进度错误
QPlayAuto_NoListenRight = 122, //无法播放(无版权)
QPlayAuto_SongNotMatch=123, //歌曲不匹配
QPlayAuto_Disconnect=124, //连接已断开
};
/**
QPlayAuto连接状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoConnectState)
{
QPlayAutoConnectState_Connected = 0, //连接成功
QPlayAutoConnectState_Disconnect = 1, //断开连接
QPlayAutoConnectState_Failed = -1, //连接失败
QPlayAutoConnectState_Cancel = -2, //连接取消
};
/*
QPlayAutoListItem类型
*/
typedef NS_ENUM(NSUInteger, QPlayAutoListItemType)
{
QPlayAutoListItemType_Song = 1, //歌曲
QPlayAutoListItemType_Normal = 2, //普通目录
QPlayAutoListItemType_Radio = 3, //电台
QPlayAutoListItemType_Folder = 4, //歌单
QPlayAutoListItemType_Album = 5, //专辑
};
typedef NS_ENUM(NSUInteger, QPlayAutoSongListType)
{
QPlayAutoSongListType_Daily = 108, //每日30首
};
/**
QPlayAuto播放状态
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayState)
{
QPlayAutoPlayState_Stop=0, //停止
QPlayAutoPlayState_Playing=1, //播放
QPlayAutoPlayState_Pause=2 //暂停
};
typedef NS_ENUM(NSInteger, QPlayAutoAssenceMode)
{
QPlayAutoAssenceMode_Full = 0, // 默认整首播放
QPlayAutoAssenceMode_Part = 1, // 仅播放精华片段
};
/**
QPlayAuto播放模式
*/
typedef NS_ENUM(NSUInteger, QPlayAutoPlayMode)
{
QPlayAutoPlayMode_SequenceCircle=0, //列表循环
QPlayAutoPlayMode_SingleCircle=1, //单曲循环
QPlayAutoPlayMode_RandomCircle=2 //随机播放
};
/**
QPlayAuto搜索类型
*/
typedef NS_ENUM(NSInteger, QPlayAutoSearchType)
{
QPlayAutoSearchType_Song = 0, //歌曲
QPlayAutoSearchType_Album = 2, //专辑
QPlayAutoSearchType_Folder = 3, //歌单
QPlayAutoSearchType_Composite = 100, //综合
};
typedef NS_ENUM(NSInteger, QPlayAutoVipState)
{
QPlayAutoVipState_Normal = 0,
QPlayAutoVipState_VIP = 1,
QPlayAutoVipState_SVIP = 2,
};
/**
第三方App信息
*/
@interface QPlayAutoAppInfo : NSObject<NSCoding>
@property (nonatomic,strong) NSString *deviceId; //AppId
@property (nonatomic,assign) NSInteger deviceType;
@property (nonatomic,strong) NSString *scheme; //跳转scheme
@property (nonatomic,strong) NSString *brand; //品牌
@property (nonatomic,strong) NSString *name; //名称
@property (nonatomic,strong) NSString *appId; //名称
@property (nonatomic,strong) NSString *secretKey; //私钥
@property (nonatomic,strong) NSString *bundleId; //bundleId
@property (nonatomic,assign) NSInteger qmCommandPort; //Q音接收命令端口
@property (nonatomic,strong) NSString *qmHost; //Q音接收命令IP
@property (nonatomic,strong) NSDate *lastConnectDate;//上次连接的日期
@end
@interface QPlayAutoSinger : NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Mid; //Mid
@property (nonatomic,strong) NSString *name; //名称
@end
/*
QPlayAuto列表项(歌单、电台、排行榜、歌曲、etc)
*/
@interface QPlayAutoListItem: NSObject
@property (nonatomic,strong) NSString *ID; //ID
@property (nonatomic,strong) NSString *Name; //名称
@property (nonatomic,strong) NSString *SubName; //副标题
@property (nonatomic,strong) QPlayAutoSinger *singer; //歌手
@property (nonatomic,strong) NSString *Album; //专辑
@property (nonatomic,strong) NSString *Mid;
@property (nonatomic,assign) QPlayAutoVipState vipState;
@property (nonatomic,assign) QPlayAutoListItemType Type;
@property (nonatomic,assign) NSInteger Duration; //时长
@property (nonatomic,strong) NSString *CoverUri; //封面Uri
@property (nonatomic,assign) NSInteger totalCount; //总数
@property (nonatomic,assign) BOOL isFav; //是否收藏
@property (nonatomic,assign) BOOL isOrigin; //是否原唱
@property (nonatomic,assign) BOOL isNoAudioSource; //无音源
@property (nonatomic,strong) NSMutableArray<QPlayAutoListItem*> *items; //子列表
@property (nonatomic,weak) QPlayAutoListItem *parentItem; //父节点
@property (nonatomic,assign) BOOL isTryListen; //是否试听歌曲
@property (nonatomic,readonly) BOOL isSong; //歌曲
@property (nonatomic,readonly) BOOL isFolder;//歌单
@property (nonatomic,readonly) BOOL isAlbum; //专辑
@property (nonatomic,readonly) BOOL isRadio; //电台
@property (nonatomic,readonly) BOOL isDirectory; //普通目录
@property (nonatomic,readonly) BOOL isVIP; //VIP歌曲
@property (nonatomic,readonly) BOOL isSVIP; //SVIP歌曲
- (instancetype)initWithDictionary:(NSDictionary*)dict;
- (instancetype)initSongWithId:(NSString *)identifier isFav:(BOOL)isFav;
- (instancetype)initWithId:(NSString *)identifier type:(QPlayAutoListItemType)type;
//是否是根节点
- (BOOL)isRoot;
//是否有更多
- (BOOL)hasMore;
- (QPlayAutoListItem*)findItemWithID:(NSString*)ID;
@end
/**
QPlayAuto请求回调
@param success 是否成功
@param dict 结果Dictionary
*/
typedef void (^QPlayAutoRequestFinishBlock)(BOOL success, NSDictionary *dict);
@interface QPlayAutoRequestBase : NSObject
/** 请求类型 */
@property (nonatomic, assign) int type;
@end
@interface QPlayAutoSentence : NSObject <NSCoding>
@property (nonatomic, assign) NSInteger startTime;
@property (nonatomic, assign) NSInteger duration;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, readonly) NSString *startTimeText;
@property (nonatomic, readonly) NSString *endTimeText;
@end
@interface QPlayAutoLyric : NSObject <NSCoding>
@property(nonatomic,assign) BOOL isTxtType;//歌词是不是纯文本的歌词,没有时间标记
@property(nonatomic,strong) NSString *songId;
@property(nonatomic,strong) NSString *songName;
@property(nonatomic,strong) NSString *singerName;
@property(nonatomic,strong) NSString *albumName;
@property(nonatomic,readonly) NSString *text;//所有歌词文本
@property(nonatomic,strong) NSArray<QPlayAutoSentence *> *sentences;
- (NSString *)sentenceAtTime:(NSTimeInterval)time;
@end
#endif /* QPlayAutoDefine_h */

188
apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/QPlayAutoSDK.h

@ -1,188 +0,0 @@
//
// QPlayAutoSDK.h
// QPlayAutoSDK
//
// Created by travisli(李鞠佑) on 2018/11/5.
// Copyright © 2018年 腾讯音乐. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QPlayAutoDefine.h"
#define APP_DEVICE_TYPE (3)
NS_ASSUME_NONNULL_BEGIN
typedef void (^QPlayAutoResultItemsBlock)(NSInteger errorCode,NSArray<QPlayAutoListItem *> *_Nullable items);
typedef void (^QPlayAutoResultParentItemBlock)(NSInteger errorCode,QPlayAutoListItem *parentItem);
extern NSString const * QPlayAutoSDKVersion;
@protocol QPlayAutoSDKDelegate <NSObject>
//连接状态变化回调
- (void)onQPlayAutoConnectStateChanged:(QPlayAutoConnectState)newState;
//播放状态状态变化回调
- (void)onQPlayAutoPlayStateChanged:(QPlayAutoPlayState)playState song:(QPlayAutoListItem *)song position:(NSInteger)position;
//歌曲播放进度
- (void)onQPlayAutoPlayProgressChanged:(QPlayAutoListItem *)song progress:(NSTimeInterval)progress duration:(NSTimeInterval)duration;
//歌曲收藏状态变化
- (void)onSongFavoriteStateChange:(NSString*)songID isFavorite:(BOOL)isFavorite;
//播放状态事件变化
- (void)onPlayModeChange:(QPlayAutoPlayMode)playMode;
//定时关闭事件
- (void)onPlayPausedByTimeoff;
//登陆状态
- (void)onLoginStateDidChanged:(BOOL)isLoginOK;
@end
@interface QPlayAutoSDK : NSObject
/// 注册App
/// @param name 名称
/// @param brand 品牌
/// @param deviceId ?
/// @param scheme 授权成功后Q音跳转合作方的scheme
/// @param appId Q音分配的Id
/// @param secretKey 合作方私钥
/// @param bundleId 合作方包名
+ (void)registerAppWithName:(NSString *)name brand:(NSString *)brand deviceId:(NSString *)deviceId scheme:(NSString *)scheme appId:(NSString *)appId secretKey:(NSString *)secretKey bundleId:(NSString *)bundleId;
/// 播放状态
+ (QPlayAutoPlayMode)currentPlayMode;
/// 当前歌曲
+ (QPlayAutoListItem *_Nullable)currentSong;
/// 当前播放状态
+ (QPlayAutoPlayState)currentPlayState;
/// 当前播放进度
+ (NSTimeInterval)currentProgress;
/// QQ音乐是否登录
+ (BOOL)isLoginOK;
/// 检查QQ音乐是否已安装
+ (BOOL)isQQMusicInstalled;
/// 是否已连接
+ (BOOL)isConnected;
/// 连接中
+ (BOOL)isConnecting;
///QQ音乐设备信息
+ (NSDictionary *)deviceInfo;
+(NSString *)openId;
+(NSString *)openToken;
///是否可以处理回调url
+ (BOOL)canHandleOpenURL:(NSURL *)url;
///处理回调url
+ (BOOL)handleOpenURL:(NSURL *)url;
/// 回调代理
+ (void)setDelegate:(id<QPlayAutoSDKDelegate>) delegate;
/// 拉端连接QPlay
/// - Parameter forceLogin: 是否强制登录
+ (void)connectAndForceLogin:(BOOL)forceLogin;
/// 停止
+ (void)stop;
/// 二次热启动连接(需要联系Q音打开此功能才能使用)
/// - Parameters:
/// - timeout: 超时时间
/// - completion: 回调
+ (void)reconnectWithTimeout:(NSTimeInterval)timeout completion:(QPlayAutoRequestFinishBlock)completion;
/// 跳转QQ音乐登录
/// @param bundleId 合作方bundleId
/// @param callbackUrl 跳转合作方url
+ (BOOL)loginQQMusicWithBundleId:(NSString *)bundleId callbackUrl:(NSString *)callbackUrl;
/// 获取数据
/// @param parent 父
/// @param pageIndex 页码
/// @param pageSize 页大小
/// @param completion 回调
+ (void)getDataItemsFromParent:(QPlayAutoListItem *)parent pageIndex:(NSUInteger)pageIndex pageSize:(NSUInteger)pageSize completion:(QPlayAutoResultParentItemBlock)completion;
/// 设置播放模式
/// @param playMode 播放模式
/// @param block 回调
+ (NSInteger)setPlayMode:(QPlayAutoPlayMode)playMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 设置播放整首还是高潮
/// @param assencceMode 高潮还是整首
/// @param block 回调
+ (NSInteger)setAssenceMode:(QPlayAutoAssenceMode)assencceMode callback:(QPlayAutoRequestFinishBlock _Nullable )block;
/// 收藏/取消收藏(歌曲id)
/// @param song 歌曲
/// @param isFavorite 收藏/取消收藏
/// @param completion 回调
+ (void)setFavoriteStateWithSong:(QPlayAutoListItem *)song isFavorite:(BOOL)isFavorite completion:(void (^)(NSInteger errorCode))completion;
/// 播放歌曲
/// @param songList 歌曲列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playAtIndex:(NSArray<QPlayAutoListItem*> *)songList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
/// 通过歌曲Mid列表播放歌曲
/// @param songMidList 歌曲Mid列表
/// @param playIndex 播放索引
/// @param completion 回调
+ (void)playSongMidAtIndex:(NSArray<NSString*> *)songMidList playIndex:(NSInteger)playIndex completion:(void (^)(NSInteger errorCode))completion;
///播放下一首
+ (void)playerPlayNextWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放上一首
+ (void)playerPlayPrevWithCompletion:(void (^)(NSInteger errorCode))completion;
///播放暂停
+ (void)playerPlayPause;
///播放恢复
+ (void)playerPlayResumeWithCompletion:(void (^)(NSInteger errorCode))completion;
///Seek到指定位置
+ (void)playerSeek:(NSInteger)position;
/// 搜索歌曲
/// @param keyword 关键词
/// @param type 搜索类型
/// @param firstPage 是否第一页
/// @param completion 回调
+ (NSInteger)search:(NSString *)keyword
type:(QPlayAutoSearchType)type
firstPage:(BOOL)firstPage
completion:(QPlayAutoResultItemsBlock)completion;
/// 相似歌曲
/// @param song 歌曲
/// @param completion 回调
+ (void)requestSimilarWithSong:(QPlayAutoListItem *)song completion:(QPlayAutoResultItemsBlock)completion;
/// 查询歌词
/// @param song 歌曲
/// @param completion 回调
+ (void)requestLyricWithSong:(QPlayAutoListItem *)song completion:(void (^)(NSInteger errorCode,QPlayAutoLyric *_Nullable lyric))completion;
/// 获取特定歌曲列表
/// @param type 列表类型
/// @param completion 回调
+ (void)requestSonglistWithType:(QPlayAutoSongListType)type completion:(QPlayAutoResultItemsBlock)completion;
@end
NS_ASSUME_NONNULL_END

5
apps/client/local_plugins/qq_music/ios/qq_music/Sources/qq_music/include/module.modulemap

@ -1,5 +0,0 @@
module QPlayAutoSDK {
header "QPlayAutoSDK.h"
header "QPlayAutoDefine.h"
export *
}

311
apps/client/local_plugins/qq_music/lib/qq_music.dart

@ -1,311 +0,0 @@
import 'package:flutter/services.dart';
class QqMusic {
static final QqMusic _instance = QqMusic._internal();
/// 获取单例
factory QqMusic() => _instance;
// 方法通道
final MethodChannel _methodChannel = const MethodChannel(
'com.yunqiinnovation.qq_music',
);
// 内部构造函数
QqMusic._internal() {}
/// 登陆qq音乐
/// 返回登录结果,包含 openId, openToken, expireTime
Future<Map<String, dynamic>> login() async {
try {
final result = await _methodChannel.invokeMethod('login');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '登录失败', 'code': e.code};
}
}
/// 搜索歌曲
/// [keyword] 搜索关键词
/// [searchType] 搜索类型 0-歌曲 1-专辑 2-歌手
Future<Map<String, dynamic>> searchSongs({
required String keyword,
int searchType = 7, // Data.SearchType.SEARCH_TYPE_SONG
}) async {
try {
final result = await _methodChannel.invokeMethod('searchSongs', {
'keyword': keyword,
'searchType': searchType,
});
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '搜索失败', 'code': e.code};
}
}
/// 播放歌曲列表
/// [songMids] 歌曲MID列表
/// [index] 开始播放的索引,默认为0
Future<Map<String, dynamic>> playSongs({
required List<String> songMids,
int index = 0,
}) async {
try {
final result = await _methodChannel.invokeMethod('playSongs', {
'songMids': songMids,
'index': index,
});
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '播放失败', 'code': e.code};
}
}
/// 播放音乐
Future<Map<String, dynamic>> playMusic() async {
try {
final result = await _methodChannel.invokeMethod('playMusic');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '播放失败', 'code': e.code};
}
}
/// 暂停音乐
Future<Map<String, dynamic>> pauseMusic() async {
try {
final result = await _methodChannel.invokeMethod('pauseMusic');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '暂停失败', 'code': e.code};
}
}
/// 自动暂停音乐
Future<Map<String, dynamic>> autopauseMusic() async {
try {
final result = await _methodChannel.invokeMethod('autopauseMusic');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '自动暂停失败', 'code': e.code};
}
}
/// 恢复播放
Future<Map<String, dynamic>> resumeMusic() async {
try {
final result = await _methodChannel.invokeMethod('resumeMusic');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '恢复播放失败', 'code': e.code};
}
}
/// 停止音乐
Future<Map<String, dynamic>> stopMusic() async {
try {
final result = await _methodChannel.invokeMethod('stopMusic');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {'success': false, 'error': e.message ?? '停止失败', 'code': e.code};
}
}
/// 下一首
Future<Map<String, dynamic>> skipToNext() async {
try {
final result = await _methodChannel.invokeMethod('skipToNext');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'success': false,
'error': e.message ?? '切换下一首失败',
'code': e.code,
};
}
}
/// 上一首
Future<Map<String, dynamic>> skipToPrevious() async {
try {
final result = await _methodChannel.invokeMethod('skipToPrevious');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'success': false,
'error': e.message ?? '切换上一首失败',
'code': e.code,
};
}
}
/// 获取播放状态
Future<Map<String, dynamic>> getPlaybackState() async {
try {
final result = await _methodChannel.invokeMethod('getPlaybackState');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'success': false,
'error': e.message ?? '获取播放状态失败',
'code': e.code,
};
}
}
/// 获取当前播放歌曲
Future<Map<String, dynamic>> getCurrentSong() async {
try {
final result = await _methodChannel.invokeMethod('getCurrentSong');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'success': false,
'error': e.message ?? '获取当前歌曲失败',
'code': e.code,
};
}
}
/// 获取播放时长
Future<Map<String, dynamic>> getPlayTime() async {
try {
final result = await _methodChannel.invokeMethod('getPlayTime');
return Map<String, dynamic>.from(result);
} on PlatformException catch (e) {
return {
'success': false,
'error': e.message ?? '获取播放时长失败',
'code': e.code,
};
}
}
/// 同步当前播放信息
Future<Map<String, dynamic>> syncCurrentPlayInfo() async {
try {
final result = await _methodChannel.invokeMethod('syncCurrentPlayInfo');
return Map<String, dynamic>.from(result);
} catch (e) {
throw Exception('同步播放信息失败: $e');
}
}
/// 获取平台版本
Future<String?> getPlatformVersion() async {
try {
final version = await _methodChannel.invokeMethod<String>(
'getPlatformVersion',
);
return version;
} on PlatformException catch (e) {
throw Exception('获取平台版本失败: ${e.message}');
}
}
}
/// 登录结果类
class QQMusicLoginResult {
final bool success;
final String? openId;
final String? openToken;
final String? expireTime;
final String? error;
final String? code;
QQMusicLoginResult({
required this.success,
this.openId,
this.openToken,
this.expireTime,
this.error,
this.code,
});
factory QQMusicLoginResult.fromMap(Map<String, dynamic> map) {
return QQMusicLoginResult(
success: map['success'] ?? false,
openId: map['openId'],
openToken: map['openToken'],
expireTime: map['expireTime'],
error: map['error'],
code: map['code'],
);
}
}
/// 搜索结果类
class QQMusicSearchResult {
final bool success;
final List<QQMusicSong>? songs;
final String? error;
final String? code;
QQMusicSearchResult({
required this.success,
this.songs,
this.error,
this.code,
});
factory QQMusicSearchResult.fromMap(Map<String, dynamic> map) {
List<QQMusicSong>? songs;
if (map['songs'] != null) {
songs = (map['songs'] as List)
.map((song) => QQMusicSong.fromMap(Map<String, dynamic>.from(song)))
.toList();
}
return QQMusicSearchResult(
success: map['success'] ?? false,
songs: songs,
error: map['error'],
code: map['code'],
);
}
}
/// 歌曲信息类
class QQMusicSong {
final String? mid;
final String? id;
final String? name;
final String? singer;
final String? album;
final int? duration;
final String? pic;
QQMusicSong({
this.mid,
this.id,
this.name,
this.singer,
this.album,
this.duration,
this.pic,
});
factory QQMusicSong.fromMap(Map<String, dynamic> map) {
return QQMusicSong(
mid: map['mid'],
id: map['id'],
name: map['name'],
singer: map['singer'],
album: map['album'],
duration: map['duration'],
pic: map['pic'],
);
}
Map<String, dynamic> toMap() {
return {
'mid': mid,
'id': id,
'name': name,
'singer': singer,
'album': album,
'duration': duration,
'pic': pic,
};
}
}

28
apps/client/local_plugins/qq_music/pubspec.yaml

@ -1,28 +0,0 @@
name: qq_music
description: "A new Flutter project."
version: 0.0.1
homepage:
environment:
sdk: ^3.8.1
flutter: '>=3.3.0'
dependencies:
flutter:
sdk: flutter
plugin_platform_interface: ^2.0.2
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
plugin:
platforms:
android:
package: com.yunqiinnovation.qq_music
pluginClass: QqMusicPlugin
ios:
pluginClass: QqMusicPlugin

7
apps/client/pubspec.lock

@ -1541,13 +1541,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.6.0" version: "1.6.0"
qq_music:
dependency: "direct main"
description:
path: "local_plugins/qq_music"
relative: true
source: path
version: "0.0.1"
qr: qr:
dependency: transitive dependency: transitive
description: description:

2
apps/client/pubspec.yaml

@ -145,8 +145,6 @@ dependencies:
path: local_plugins/location_service path: local_plugins/location_service
user_portrait: user_portrait:
path: local_plugins/user_portrait path: local_plugins/user_portrait
qq_music:
path: local_plugins/qq_music
floating_ui_plugin: floating_ui_plugin:
path: local_plugins/floating_ui_plugin path: local_plugins/floating_ui_plugin
encrypt: ^5.0.3 encrypt: ^5.0.3

Loading…
Cancel
Save