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