Browse Source
用户决定整个删掉(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
83 changed files with 12 additions and 9128 deletions
@ -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()}'}; |
|||
} |
|||
} |
|||
} |
|||
@ -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}\"}" |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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) ?? "{}" |
|||
} |
|||
} |
|||
@ -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/ |
|||
@ -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' |
|||
@ -1,3 +0,0 @@ |
|||
## 0.0.1 |
|||
|
|||
* TODO: Describe initial release. |
|||
@ -1 +0,0 @@ |
|||
TODO: Add your license here. |
|||
@ -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. |
|||
|
|||
@ -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 |
|||
@ -1,9 +0,0 @@ |
|||
*.iml |
|||
.gradle |
|||
/local.properties |
|||
/.idea/workspace.xml |
|||
/.idea/libraries |
|||
.DS_Store |
|||
/build |
|||
/captures |
|||
.cxx |
|||
@ -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') |
|||
} |
|||
} |
|||
Binary file not shown.
@ -1 +0,0 @@ |
|||
rootProject.name = 'qq_music' |
|||
@ -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> |
|||
@ -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); |
|||
} |
|||
@ -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); |
|||
} |
|||
@ -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; |
|||
|
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
} |
|||
@ -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" |
|||
} |
|||
} |
|||
} |
|||
@ -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 |
|||
) |
|||
} |
|||
@ -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() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
|
|||
} |
|||
@ -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() |
|||
} |
|||
@ -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; |
|||
} |
|||
} |
|||
@ -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) |
|||
} |
|||
} |
|||
@ -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}]") |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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> |
|||
|
Before Width: | Height: | Size: 2.2 KiB |
@ -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> |
|||
@ -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> |
|||
@ -1,4 +0,0 @@ |
|||
<resources> |
|||
<dimen name="fab_margin">16dp</dimen> |
|||
<dimen name="dimen_width">80dp</dimen> |
|||
</resources> |
|||
@ -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> |
|||
@ -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> |
|||
@ -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 |
|||
@ -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 |
|||
@ -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> |
|||
@ -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 */ |
|||
@ -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 |
|||
@ -1,8 +0,0 @@ |
|||
module QPlayAutoSDK { |
|||
header "QPlayAutoSDK.h" |
|||
header "QPlayAutoDefine.h" |
|||
|
|||
export * |
|||
|
|||
link "QPlayAutoSDK" |
|||
} |
|||
Binary file not shown.
@ -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 */ |
|||
@ -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 |
|||
@ -1,8 +0,0 @@ |
|||
module QPlayAutoSDK { |
|||
header "QPlayAutoSDK.h" |
|||
header "QPlayAutoDefine.h" |
|||
|
|||
export * |
|||
|
|||
link "QPlayAutoSDK" |
|||
} |
|||
Binary file not shown.
@ -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" |
|||
) |
|||
] |
|||
) |
|||
@ -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 |
|||
} |
|||
} |
|||
@ -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]) |
|||
} |
|||
} |
|||
@ -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 */ |
|||
@ -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 |
|||
@ -1,5 +0,0 @@ |
|||
module QPlayAutoSDK { |
|||
header "QPlayAutoSDK.h" |
|||
header "QPlayAutoDefine.h" |
|||
export * |
|||
} |
|||
@ -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, |
|||
}; |
|||
} |
|||
} |
|||
@ -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 |
|||
Loading…
Reference in new issue