diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index de01cd468..d26cbc350 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -5,6 +5,7 @@ repositories { mavenCentral() flatDir { dirs(project(":spotify_sdk").projectDir.resolve("libs")) + dirs(project(":qq_music").projectDir.resolve("libs")) // 添加这行 } } diff --git a/android/app/sign/DeepSound.properties b/android/app/sign/DeepSound.properties deleted file mode 100644 index 96bb7e16a..000000000 --- a/android/app/sign/DeepSound.properties +++ /dev/null @@ -1,4 +0,0 @@ -storePassword=13071376716 -keyPassword=13071376716 -keyAlias=DeepSound -storeFile=sign/DeepSound.keystore \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ebb128fb5..ceba71e9f 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -96,7 +96,7 @@ - + + + diff --git a/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl b/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl new file mode 100644 index 000000000..718a193aa --- /dev/null +++ b/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl @@ -0,0 +1,15 @@ +// IPlayService.aidl +package com.yunqiinnovation.qq_music; + +// Declare any non-default types here with import statements + +import com.yunqiinnovation.qq_music.IPrint; + +interface IPlayService { + void bindService(); + void startPcmMode(); + void stopPcmMode(); + void resumeOrPause(); + void playNext(); + void setPrintMessageCallback(IPrint callback); +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl b/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl new file mode 100644 index 000000000..70d14f41c --- /dev/null +++ b/local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl @@ -0,0 +1,8 @@ +// IPrintMessage.aidl +package com.yunqiinnovation.qq_music; + +// Declare any non-default types here with import statements + +interface IPrint { + void print(String msg); +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/ApiSample.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/ApiSample.kt new file mode 100644 index 000000000..1f1fb70d7 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/ApiSample.kt @@ -0,0 +1,361 @@ +package com.tencent.qqmusic.api.demo + +import android.os.Bundle +import androidx.annotation.Nullable +import android.util.Log +import com.google.gson.Gson +import com.tencent.qqmusic.third.api.contract.* +import com.tencent.qqmusic.third.api.contract.ErrorCodes.ERROR_API_NOT_INITIALIZED + + +// +// Created by tylertan on 2020-03-19. +// Copyright (c) 2020 Tencent. All rights reserved. +// + +/** + * 对[Methods]中的接口进行基础封装 + * + */ +object ApiSample { + + const val TAG = "ApiSample" + + /** + * 获取接口返回错误码,对应[ErrorCodes]中的错误码列表 + * + * @return 返回[ErrorCodes.ERROR_OK]代表成功,反之为失败 + */ + private fun apiCode(bundle: Bundle?): Int? { + return bundle?.getInt(Keys.API_RETURN_KEY_CODE) + } + + /** + * 获取接口返回错误详细信息 + * + */ + private fun apiError(bundle: Bundle?): String? { + return bundle?.getString(Keys.API_RETURN_KEY_ERROR) + } + + fun hi(api: IQQMusicApi?) { + 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 = api?.execute("hi", bundle) + Log.i(TAG, "sayHi ret:" + result!!.getInt(Keys.API_RETURN_KEY_CODE)) + } + + fun playSongMid(api: IQQMusicApi?, ids: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("midList", ids) + api?.executeAsync("playSongMid", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + private fun playSongMidAtIndex(api: IQQMusicApi?, songList: List, index: Int, block: (() -> Unit)) { + val midList = ArrayList() + for (i in songList.indices) { + midList.add(songList[i].mid) + } + + val params = Bundle() + params.putStringArrayList("midList", midList) + params.putInt("index", index) + Log.i(TAG, "[playSongMidAtIndex] 播放歌曲列表... $index,${songList.size}") + api?.executeAsync("playSongMidAtIndex", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun playSongLocalPath(api: IQQMusicApi?, pathList: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("pathList", pathList) + api?.executeAsync("playSongLocalPath", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun playMusic(api: IQQMusicApi?): Int { + val ret = api?.execute("playMusic", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun stopMusic(api: IQQMusicApi?): Int { + val ret = api?.execute("stopMusic", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun pauseMusic(api: IQQMusicApi?): Int { + val ret = api?.execute("pauseMusic", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun resumeMusic(api: IQQMusicApi?): Int { + val ret = api?.execute("resumeMusic", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun skipToNext(api: IQQMusicApi?): Int { + val ret = api?.execute("skipToNext", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun skipToPrevious(api: IQQMusicApi?): Int { + val ret = api?.execute("skipToPrevious", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + fun getPlaybackState(api: IQQMusicApi?): Int { + val ret = api?.execute("getPlaybackState", null) + return ret?.getInt(Keys.API_RETURN_KEY_CODE) ?: 0 + } + + @Nullable + fun getCurrentSong(api: IQQMusicApi?, gson: Gson): Data.Song? { + val ret = api?.execute("getCurrentSong", null) + val curSongJson = ret?.getString(Keys.API_RETURN_KEY_DATA) + return gson.fromJson(curSongJson, Data.Song::class.java) + } + + fun addToFavourite(api: IQQMusicApi?, midList: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("midList", midList) + api?.executeAsync("addToFavourite", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun removeFromFavourite(api: IQQMusicApi?, midList: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("midList", midList) + api?.executeAsync("removeFromFavourite", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun addLocalPathToFavourite(api: IQQMusicApi?, localPathList: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("localPathList", localPathList) + api?.executeAsync("addLocalPathToFavourite", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun removeLocalPathFromFavourite(api: IQQMusicApi?, localPathList: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("localPathList", localPathList) + api?.executeAsync("removeLocalPathFromFavourite", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun isFavouriteMid(api: IQQMusicApi?, midList: ArrayList, block: ((ret: BooleanArray?) -> Unit)) { + val params = Bundle() + params.putStringArrayList("midList", midList) + api?.executeAsync("isFavouriteMid", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + val ret = result.getBooleanArray(Keys.API_RETURN_KEY_DATA) + block(ret) + } + }) + } + + fun isFavouriteLocalPath(api: IQQMusicApi?, localPathList: ArrayList, block: ((ret: BooleanArray?) -> Unit)) { + val params = Bundle() + params.putStringArrayList("localPathList", localPathList) + api?.executeAsync("isFavouriteLocalPath", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + val ret = result.getBooleanArray(Keys.API_RETURN_KEY_DATA) + block(ret) + } + }) + } + + fun registerEventListener(api: IQQMusicApi?, events: ArrayList, block: (() -> Unit)) { + api?.registerEventListener(events, object : IQQMusicApiEventListener.Stub() { + override fun onEvent(event: String, extra: Bundle) { + block() + } + }) + } + + fun unregisterEventListener(api: IQQMusicApi?, events: ArrayList, block: (() -> Unit)) { + api?.unregisterEventListener(events, object : IQQMusicApiEventListener.Stub() { + override fun onEvent(event: String, extra: Bundle) { + block() + } + }) + } + + fun playFromChorus(api: IQQMusicApi?, fromChorus: Boolean) { + val params = Bundle() + params.putBoolean("fromChorus", fromChorus) + api?.execute("playFromChorus", params) + } + + @Deprecated("Use playSongMid instead.") + fun playSongId(api: IQQMusicApi?, ids: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("songIdList", ids) + api?.executeAsync("playSongId", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + @Deprecated("Use playSongMidAtIndex instead.") + fun playSongIdAtIndex(api: IQQMusicApi?, ids: ArrayList, block: (() -> Unit)) { + val params = Bundle() + params.putStringArrayList("songIdList", ids) + api?.executeAsync("playSongIdAtIndex", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + block() + } + }) + } + + fun getTotalTime(api: IQQMusicApi?): Long { + val result = api?.execute("getTotalTime", null) + return (result?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + } + + fun getCurrTime(api: IQQMusicApi?): Long { + val result = api?.execute("getCurrTime", null) + return (result?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + } + + fun getPlayList(api: IQQMusicApi?, ids: ArrayList, block: ((json: String?) -> Unit)) { + val params = Bundle() + params.putStringArrayList("songIdList", ids) + api?.executeAsync("getPlayList", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle) { + val json = result.getString(Keys.API_RETURN_KEY_DATA) + block(json) + } + }) + } + + fun getFolderList(api: IQQMusicApi?, folderId: String, folderType: Int, page: Int, block: (json: String?) -> Unit) { + val params = Bundle().apply { + putString("folderId", folderId) + putInt("folderType", folderType) + putInt("page", page) + } + + api?.executeAsync("getFolderList", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val json = p0?.getString(Keys.API_RETURN_KEY_DATA) + block(json) + } + }) + } + + fun getSongList(api: IQQMusicApi?, folderId: String, folderType: Int, page: Int, block: (json: String?) -> Unit) { + val params = Bundle().apply { + putString("folderId", folderId) + putInt("folderType", folderType) + putInt("page", page) + } + + api?.executeAsync("getSongList", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val json = p0?.getString(Keys.API_RETURN_KEY_DATA) + block(json) + } + }) + } + + fun getUserFolderList(api: IQQMusicApi?, openId: String, openToken: String, folderId: String, folderType: Int, page: Int, block: (json: String?) -> Unit) { + val params = Bundle().apply { + putString("openId", openId) + putString("openToken", openToken) + putString("folderId", folderId) + putInt("folderType", folderType) + putInt("page", page) + } + + api?.executeAsync("getUserFolderList", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val json = p0?.getString(Keys.API_RETURN_KEY_DATA) + block(json) + } + }) + } + + fun getUserSongList(api: IQQMusicApi?, openId: String, openToken: String, folderId: String, folderType: Int, page: Int, block: (json: String?) -> Unit) { + val params = Bundle().apply { + putString("openId", openId) + putString("openToken", openToken) + putString("folderId", folderId) + putInt("folderType", folderType) + putInt("page", page) + } + + api?.executeAsync("getUserSongList", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val json = p0?.getString(Keys.API_RETURN_KEY_DATA) + block(json) + } + }) + } + + fun search(api: IQQMusicApi?, block: () -> Unit) { + val params = Bundle().apply { + putString("keyword", "周杰伦") + putInt("searchType", 0) + putBoolean("firstPage", true) + } + + api?.executeAsync("search", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + block() + } + }) + } + + fun voiceShortcut(api: IQQMusicApi?, intent: String, block: (code: Int?) -> Unit) { + val bundle = Bundle() + bundle.apply { + putString("intent", intent) + } + api?.executeAsync("voiceShortcut", bundle, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val code = p0?.getInt(Keys.API_RETURN_KEY_CODE) ?: ERROR_API_NOT_INITIALIZED + block(code) + } + }) + } + + fun voicePlay(api: IQQMusicApi?, query: String, slotList: ArrayList, block: (code: Int, json: String?) -> Unit) { + val bundle = Bundle() + bundle.apply { + putString("query", query) + putStringArrayList("slotList", slotList) + } + api?.executeAsync("voicePlay", bundle, object : IQQMusicApiCallback.Stub() { + override fun onReturn(p0: Bundle?) { + val code = p0?.getInt(Keys.API_RETURN_KEY_CODE) ?: ERROR_API_NOT_INITIALIZED + val json = p0?.getString(Keys.API_RETURN_KEY_DATA) + block(code, json) + } + }) + } + +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java new file mode 100644 index 000000000..b27ae44ea --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java @@ -0,0 +1,53 @@ +package com.yunqiinnovation.qq_music; + +import com.tencent.qqmusic.third.api.contract.CommonCmd; + +public class Config { + + /** + * 需向QQ音乐发邮件申请,具体参考README + */ + public static final String OPENID_APPID = "2000000921"; + + /** + * 配置接入方自己生成的RSA私钥,由于是Demo,所以直接写在代码中了,真实环境中需要注意私钥泄漏的风险。 + */ + public static final String OPENID_APP_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n" + + "MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALWuTayxg0296yfm\n" + + "h8WhunybY1BEFt9fB4qb3TGvB0vkd+oKDoBdiTevtgwSL47h4O0ISNTQqqmVOvk1\n" + + "EQMGcLESBEdHquPX2uOplwmmVWx1un9NYB4JKaHmUkCXOIoiCgmEY052zFY2T+a0\n" + + "RfFiaUTc0wRMynquE8Qp18i23IUBAgMBAAECgYBf/s1D5jd+qE7MMl6i6iVjGaeM\n" + + "Da4YMFoHvvtM+TJdw0Yo/09SPBn4tnr2PdWrJWZ3V91DtU23dQZIDuYCNzDrOkVr\n" + + "93W5Pkqprdbde6qRhWm1VTZVN7h85DStGBzovSxQ6DQNHZFzIriJkfGDjPopmIil\n" + + "awQvMPlS3lLYkzHwAQJBAOWBlshEMHXeqXekjJExwX7YSy2oabWXtllCFbcPprAY\n" + + "MiM8cdNsEhnhrsmdBN4crqTQKJq/ATaYFhzASuIiZ2ECQQDKp17+A3u6q7RZ3Vmb\n" + + "khvAATQpVK5loCOzuyPU7/UGU49AO1DIaPwDvaUqL6PuF9eFrcRHA5AFbBjOgYNn\n" + + "piGhAkEAr5gMo0D4ytYHVxDAs74by071hgNajaN70qLrNlp1MDny8l6mwBnpZ3/y\n" + + "WTMXrFDJljPlPTByXzDRikHdiVLFQQJATMOTpxK4tqA+A+BmBhkVa+jI0nMiUJqV\n" + + "i4py60EuBoR2efU+B3xFIkGfI9jB9vmPCTMma2P9FSC6UNccaJG0oQJBAJYb6Ch9\n" + + "yowGPpY1CShaR+TfauUreJBd4PSpu8mFlYUssp2AXYcJM51RMzrZKXvTTOvwkStc\n" + + "NRCBcPVAYcVUnXo=\n" + + "-----END PRIVATE KEY-----"; + + /** + * 获取纯Base64格式的私钥(去除PEM头尾标记) + * @return 纯Base64格式的私钥字符串 + */ + public static String getBase64PrivateKey() { + return OPENID_APP_PRIVATE_KEY + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); // 移除所有空白字符(包括换行符) + } + + /** + * 配置平台类型,可选值如下: + * {@link CommonCmd#AIDL_PLATFORM_TYPE_PHONE } + * {@link CommonCmd#AIDL_PLATFORM_TYPE_TV} + * {@link CommonCmd#AIDL_PLATFORM_TYPE_CAR} + * {@link CommonCmd#AIDL_PLATFORM_TYPE_PAD} + */ + // 修改这一行:使用CommonCmd中的常量而不是字符串 + public static String BIND_PLATFORM = CommonCmd.AIDL_PLATFORM_TYPE_PHONE; + +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt new file mode 100644 index 000000000..6cee7e034 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt @@ -0,0 +1,240 @@ +package com.yunqiinnovation.qq_music + +import android.app.Activity +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 activity: Activity) { + + companion object { + private const val TAG = "QQMusicLoginManager" + private const val BASE_SCHEME = "voitrans://" + private const val URI_LOGIN = BASE_SCHEME + "login" + + // 定义错误码常量,替代 LoginExampleActivity 中的常量 + const val LOGIN_FAILED = 1 + } + + 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(openId: String, openToken: String, expireTime: String) + fun onError(errorCode: String, errorMessage: String) + } + + /** + * 开始登录流程 + */ + 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(activity) + + // 开始绑定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, "已授权,直接获取登录信息") + callback.onSuccess("","","") +// requestAuth(callback) + } + ErrorCodes.ERROR_API_NO_PERMISSION -> { + // 需要授权 + Log.d(TAG, "需要授权,启动授权流程") + startAuthProcess(callback) + } + else -> { + Log.e(TAG, "检查授权状态失败: $code") + callback.onError("AUTH_CHECK_FAILED", "检查授权状态失败") + } + } + } catch (e: Exception) { + Log.e(TAG, "检查授权状态异常", e) + callback.onError("AUTH_CHECK_ERROR", "检查授权状态异常: ${e.message}") + } + } + + /** + * 启动授权流程 + */ + private fun startAuthProcess(callback: LoginCallback) { + Log.d(TAG, "启动QQ音乐登录页面") + + // 启动QQ音乐登录 + CommonCmd.loginQQMusic(activity, URI_LOGIN) + + // 等待用户登录完成后进行身份验证 + handler.postDelayed({ + verifyIdentity(callback) + }, 3000) // 给用户足够的登录时间 + } + + /** + * 验证身份 + */ + private fun verifyIdentity(callback: LoginCallback) { + Log.d(TAG, "开始身份验证") + + val time = System.currentTimeMillis() + val nonce = time.toString() + val encryptString = OpenIDHelper.getEncryptString(nonce) + + CommonCmd.verifyCallerIdentity( + activity, + Config.OPENID_APPID, + activity.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") + + 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") + + handler.post { + callback.onSuccess(openId, openToken, expireTime) + } + } else { + Log.e(TAG, "签名验证失败") + handler.post { + callback.onError("SIGN_ERROR", "签名验证失败") + } + } + } else { + Log.e(TAG, "解密失败") + handler.post { + callback.onError("DECRYPT_ERROR", "解密失败") + } + } + } catch (e: Exception) { + Log.e(TAG, "解析登录结果失败", e) + handler.post { + callback.onError("PARSE_ERROR", "解析登录结果失败: ${e.message}") + } + } + } + + /** + * 清理资源 + */ + fun cleanup() { + try { + if (::qPlayBindHelper.isInitialized) { + qPlayBindHelper.unBindQQMusic() + } + } catch (e: Exception) { + Log.e(TAG, "清理资源失败", e) + } + } +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt new file mode 100644 index 000000000..1e4f8a5c1 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt @@ -0,0 +1,645 @@ +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 activity: Activity) { + companion object { + const val TAG = "QQMusicManager" + } + + private var qqmusicApi: IQQMusicApi? = null + private var isBindQQMusicService = false + private val gson = Gson() + + // 回调接口 + interface SearchCallback { + fun onSuccess(songs: List>) + fun onError(errorCode: Int, errorMessage: String) + } + + interface PlayCallback { + fun onSuccess() + fun onError(errorCode: Int, errorMessage: String) + } + + interface InfoCallback { + fun onSuccess(data: Map) + fun onError(errorCode: Int, errorMessage: String) + } + + private val serviceConnection = 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 + } + } + + init { + bindQQMusicApiService() + } + + private fun sayHi() { + if (!isBindQQMusicService || qqmusicApi == null) { + Log.w(TAG, "QQ音乐服务未连接,无法发送hi消息") + 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() { + 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}") + return + } + } + + Log.d(TAG, "绑定Intent: action=${intent.action}, package=${intent.`package`}") + + try { + val bindResult = activity.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + Log.d(TAG, "bindService结果: $bindResult") + + if (!bindResult) { + Log.e(TAG, "绑定QQ音乐服务失败,尝试启动QQ音乐进程") + // 尝试启动QQ音乐进程 + CommonCmd.startQQMusicProcess(activity, activity.packageName) + + // 延迟重试绑定 + Handler(Looper.getMainLooper()).postDelayed({ + Log.d(TAG, "重试绑定QQ音乐服务") + activity.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + }, 2000) + } + } catch (e: Exception) { + Log.e(TAG, "绑定QQ音乐服务异常", e) + } + } + + /** + * 搜索歌曲 + * @param keyword 搜索关键词 + * @param searchType 搜索类型,参考Data.SearchType + * @param callback 回调 + */ + fun searchSongs( + keyword: String, + searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, + callback: SearchCallback + ) { + Log.d(TAG, "searchSongs: keyword=$keyword, searchType=$searchType") + + if (!isBindQQMusicService || qqmusicApi == null) { + Log.e(TAG, "QQ音乐服务未连接") + callback.onError(-1, "QQ音乐服务未连接") + return + } + + // 先测试服务连接状态 + try { + val testBundle = Bundle() + testBundle.putInt(Keys.API_PARAM_KEY_SDK_VERSION, CommonCmd.SDK_VERSION) + testBundle.putString(Keys.API_PARAM_KEY_PLATFORM_TYPE, Config.BIND_PLATFORM) + + val testResult = qqmusicApi?.execute("hi", testBundle) + val testCode = testResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + Log.d(TAG, "服务连接测试结果: $testCode") + + if (testCode != ErrorCodes.ERROR_OK) { + Log.e(TAG, "QQ音乐服务连接异常: $testCode") + val testErrorMessage = when (testCode) { + ErrorCodes.ERROR_API_NO_PERMISSION -> "没有权限访问QQ音乐服务" + ErrorCodes.ERROR_API_NOT_INITIALIZED -> "QQ音乐API未初始化" + else -> "QQ音乐服务连接异常 (错误码: $testCode)" + } + callback.onError(testCode, testErrorMessage) + return + } + } catch (e: Exception) { + Log.e(TAG, "测试服务连接失败", e) + callback.onError(-1, "测试服务连接失败: ${e.message}") + return + } + + val params = Bundle().apply { + putInt("searchType", searchType) + putString("keyword", keyword) + putBoolean("firstPage", true) + } + + Log.d(TAG, "调用QQ音乐搜索API,参数: searchType=$searchType, keyword=$keyword") + + try { + // 直接调用executeAsync,不接收返回值 + qqmusicApi?.executeAsync("search", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle?) { + Log.d(TAG, "搜索API返回结果") + + if (result == null) { + Log.e(TAG, "搜索返回结果为null") + callback.onError(-1, "搜索返回结果为null") + return + } + + val code = result.getInt(Keys.API_RETURN_KEY_CODE, -1) + Log.d(TAG, "搜索返回码: $code") + + if (code == ErrorCodes.ERROR_OK) { + try { + val dataJson = result.getString(Keys.API_RETURN_KEY_DATA) + Log.d(TAG, "搜索数据: $dataJson") + + if (dataJson != null && dataJson.isNotEmpty()) { + val array = JsonParser().parse(dataJson).asJsonArray + val songs = mutableListOf>() + + for (elem in array) { + val song = gson.fromJson(elem, Data.Song::class.java) + songs.add(mapOf( + "mid" to song.mid, + "id" to song.id, + "name" to song.title, + "singer" to song.singer?.title, + "album" to song.album?.title, + "duration" to song.duration, + "pic" to song.album?.coverUri + )) + } + Log.d(TAG, "解析完成,共 ${songs.size} 首歌曲") + callback.onSuccess(songs) + } else { + Log.e(TAG, "搜索结果数据为空") + callback.onError(code, "搜索结果数据为空") + } + } catch (e: Exception) { + Log.e(TAG, "解析搜索结果异常", e) + callback.onError(-1, "解析搜索结果失败: ${e.message}") + } + } else { + val error = result.getString(Keys.API_RETURN_KEY_ERROR) ?: "搜索失败" + Log.e(TAG, "搜索失败: code=$code, error=$error") + + // 根据实际可用的错误码提供具体的错误信息 + val errorMessage = when (code) { + ErrorCodes.ERROR_API_NO_PERMISSION -> "没有搜索权限,请检查应用授权" + ErrorCodes.ERROR_API_NOT_INITIALIZED -> "QQ音乐API未初始化,请重新登录" + else -> "搜索失败: $error (错误码: $code)" + } + callback.onError(code, errorMessage) + } + } + }) + + Log.d(TAG, "executeAsync调用完成,等待回调") + + } catch (e: Exception) { + Log.e(TAG, "调用搜索API异常", e) + callback.onError(-1, "调用搜索API异常: ${e.message}") + } + } + + /** + * 播放歌曲列表 + * @param songMids 歌曲MID列表 + * @param index 开始播放的索引 + * @param callback 回调 + */ + fun playSongs( + songMids: List, + index: Int = 0, + callback: PlayCallback + ) { + + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val params = Bundle().apply { + putStringArrayList("midList", ArrayList(songMids)) + putInt("index", index) + } + + // 直接调用executeAsync,不接收返回值 + qqmusicApi?.executeAsync("playSongMidAtIndex", params, object : IQQMusicApiCallback.Stub() { + override fun onReturn(result: Bundle?) { + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + val error = result?.getString(Keys.API_RETURN_KEY_ERROR) ?: "播放失败" + callback.onError(code, error) + } + } + }) + } + + /** + * 播放音乐 + */ + fun playMusic(callback: PlayCallback) { + + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("playMusic", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "播放失败") + } + } + + /** + * 暂停音乐 + */ + fun pauseMusic(callback: PlayCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("pauseMusic", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "暂停失败") + } + } + + /** + * 恢复播放 + */ + fun resumeMusic(callback: PlayCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("resumeMusic", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "恢复播放失败") + } + } + + /** + * 停止音乐 + */ + fun stopMusic(callback: PlayCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("stopMusic", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "停止失败") + } + } + + /** + * 下一首 + */ + fun skipToNext(callback: PlayCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("skipToNext", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "切换下一首失败") + } + } + + /** + * 上一首 + */ + fun skipToPrevious(callback: PlayCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("skipToPrevious", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + callback.onSuccess() + } else { + callback.onError(code, "切换上一首失败") + } + } + + /** + * 同步当前播放信息 - 一次性获取所有播放状态 + */ + fun syncCurrentPlayInfo(callback: InfoCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + Log.w(TAG, "QQ音乐服务未连接,无法同步播放信息") + callback.onError(-1, "QQ音乐服务未连接") + return + } + + try { + Log.d(TAG, "开始同步当前播放信息") + + // 获取当前歌曲信息 + val songResult = qqmusicApi?.execute("getCurrentSong", null) + val songCode = songResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取播放状态 + val stateResult = qqmusicApi?.execute("getPlaybackState", null) + val stateCode = stateResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取当前播放时间 + val currTimeResult = qqmusicApi?.execute("getCurrTime", null) + val currTimeCode = currTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取总时长 + val totalTimeResult = qqmusicApi?.execute("getTotalTime", null) + val totalTimeCode = totalTimeResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + Log.d(TAG, "API调用结果 - song: $songCode, state: $stateCode, currTime: $currTimeCode, totalTime: $totalTimeCode") + + // 构建返回数据 + val syncData = mutableMapOf() + + // 处理歌曲信息 + if (songCode == ErrorCodes.ERROR_OK) { + val songJson = songResult?.getString(Keys.API_RETURN_KEY_DATA) + if (!songJson.isNullOrEmpty()) { + try { + val jsonElement = JsonParser.parseString(songJson) + if (jsonElement.isJsonObject) { + val songData = jsonElement.asJsonObject + syncData["currentSong"] = mapOf( + "id" to (songData.get("id")?.asString ?: ""), + "mid" to (songData.get("mid")?.asString ?: ""), + "name" to (songData.get("title")?.asString ?: ""), + "singer" to (songData.get("singer")?.asJsonObject?.get("title")?.asString ?: ""), + "album" to (songData.get("album")?.asJsonObject?.get("title")?.asString ?: ""), + "duration" to (songData.get("duration")?.asLong ?: 0L), + "coverUri" to (songData.get("album")?.asJsonObject?.get("coverUri")?.asString ?: "") + ) + Log.d(TAG, "歌曲信息同步成功: ${songData.get("title")?.asString}") + } else { + Log.w(TAG, "返回的JSON不是有效的对象格式: $songJson") + syncData["currentSong"] = null + } + } catch (e: Exception) { + Log.e(TAG, "解析歌曲信息失败", e) + syncData["currentSong"] = null + } + } else { + syncData["currentSong"] = null + } + } else { + syncData["currentSong"] = null + } + + // 处理播放状态 - 使用正确的常量 + if (stateCode == ErrorCodes.ERROR_OK) { + val playState = stateResult?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 + val isPlaying = when (playState) { + PlayState.STARTED -> true + PlayState.PREPARED -> true + PlayState.BUFFERING -> true + PlayState.PAUSED -> false + PlayState.STOPPED -> false + PlayState.ERROR -> false + else -> false + } + syncData["isPlaying"] = isPlaying + syncData["playState"] = playState + Log.d(TAG, "播放状态同步成功: $playState (isPlaying: $isPlaying)") + } else { + syncData["isPlaying"] = false + syncData["playState"] = PlayState.STOPPED + } + + // 处理播放时间 + if (currTimeCode == ErrorCodes.ERROR_OK && totalTimeCode == ErrorCodes.ERROR_OK) { + val currentTime = (currTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + val totalTime = (totalTimeResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + + syncData["currentTime"] = currentTime + syncData["totalTime"] = totalTime + + Log.d(TAG, "播放时间同步成功: ${currentTime}s / ${totalTime}s") + } else { + syncData["currentTime"] = 0L + syncData["totalTime"] = 0L + } + + // 添加同步时间戳 + syncData["syncTimestamp"] = System.currentTimeMillis() + + Log.d(TAG, "播放信息同步完成") + callback.onSuccess(syncData) + + } catch (e: Exception) { + Log.e(TAG, "同步播放信息异常", e) + callback.onError(-1, "同步播放信息异常: ${e.message}") + } + } + + /** + * 获取播放状态 + */ + fun getPlaybackState(callback: InfoCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("getPlaybackState", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + val state = result?.getInt(Keys.API_RETURN_KEY_DATA) ?: 0 + callback.onSuccess(mapOf("state" to state)) + } else { + callback.onError(code, "获取播放状态失败") + } + } + + /** + * 获取当前播放歌曲 + */ + fun getCurrentSong(callback: InfoCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + val result = qqmusicApi?.execute("getCurrentSong", null) + val code = result?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + if (code == ErrorCodes.ERROR_OK) { + try { + val curSongJson = result?.getString(Keys.API_RETURN_KEY_DATA) + if (curSongJson != null) { + val song = gson.fromJson(curSongJson, Data.Song::class.java) + val songMap = mapOf( + "mid" to song.mid, + "id" to song.id, + "name" to song.title, + "singer" to song.singer?.title, + "album" to song.album?.title, + "duration" to song.duration, + "pic" to song.album?.coverUri + ) + callback.onSuccess(songMap) + } else { + callback.onError(-1, "当前没有播放歌曲") + } + } catch (e: Exception) { + Log.e(TAG, "解析当前歌曲信息异常", e) + callback.onError(-1, "解析歌曲信息失败: ${e.message}") + } + } else { + callback.onError(code, "获取当前歌曲失败") + } + } + + /** + * 获取播放时长 + */ + fun getPlayTime(callback: InfoCallback) { + if (!isBindQQMusicService || qqmusicApi == null) { + callback.onError(-1, "QQ音乐服务未连接") + return + } + + try { + // 获取当前播放时间 + val currResult = qqmusicApi?.execute("getCurrTime", null) + val currCode = currResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + // 获取总时长 + val totalResult = qqmusicApi?.execute("getTotalTime", null) + val totalCode = totalResult?.getInt(Keys.API_RETURN_KEY_CODE) ?: -1 + + if (currCode == ErrorCodes.ERROR_OK && totalCode == ErrorCodes.ERROR_OK) { + val currentTime = (currResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + val totalTime = (totalResult?.getLong(Keys.API_RETURN_KEY_DATA) ?: 0) / 1000 + + callback.onSuccess(mapOf( + "currentTime" to currentTime, + "totalTime" to totalTime + )) + } else { + callback.onError(-1, "获取播放时长失败") + } + } catch (e: Exception) { + Log.e(TAG, "获取播放时长异常", e) + callback.onError(-1, "获取播放时长异常: ${e.message}") + } + } + + /** + * 清理资源 + */ + fun cleanup() { + try { + if (isBindQQMusicService) { + activity.unbindService(serviceConnection) + isBindQQMusicService = false + Log.d(TAG, "QQ音乐服务已解绑") + } + } catch (e: Exception) { + Log.e(TAG, "解绑QQ音乐服务失败", e) + } + } + + + + /** + * 获取当前平台对应的QQ音乐包名 + */ + private fun getQQMusicPackageName(): String { + return when (Config.BIND_PLATFORM) { + CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> "com.tencent.qqmusic" + CommonCmd.AIDL_PLATFORM_TYPE_CAR -> "com.tencent.qqmusiccar" + CommonCmd.AIDL_PLATFORM_TYPE_TV -> "com.tencent.qqmusictv" + else -> "com.tencent.qqmusic" + } + } +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt new file mode 100644 index 000000000..97f85067f --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicSingleton.kt @@ -0,0 +1,666 @@ +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.* +import java.text.SimpleDateFormat +import java.util.* + +/** + * 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" + private const val LOGIN_WAIT_DELAY_MS = 30000L + // 添加搜索重试相关常量 + private const val SEARCH_TIMEOUT_MS = 50000L + private const val MAX_RETRY_COUNT = 3 + private const val RETRY_DELAY_MS = 1000L + + @Volatile + private var INSTANCE: QQMusicSingleton? = null + + fun getInstance(): QQMusicSingleton { + return INSTANCE ?: synchronized(this) { + INSTANCE ?: QQMusicSingleton().also { INSTANCE = it } + } + } + } + + private var activity: Activity? = null + private var loginManager: QQMusicLoginManager? = null + private var musicManager: QQMusicManager? = null + private val handler = Handler(Looper.getMainLooper()) + private var sharedPreferences: SharedPreferences? = null + private var currentLoginResult: LoginResult? = null + // 添加搜索超时跟踪 + private val searchTimeouts = mutableMapOf() + + /** + * 初始化单例,设置Activity上下文 + */ + fun initialize(activity: Activity) { + this.activity = activity + this.sharedPreferences = activity.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) + loadSavedLoginInfo() + Log.d(TAG, "QQ音乐单例初始化完成") + } + /** + * 检查QQ音乐App是否已安装 + */ + private 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.activity?.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 { + if (currentLoginResult == null){ + loadSavedLoginInfo() + if (currentLoginResult == null){ + return false + } + return true + }else{ + return true + } + } + + /** + * 获取当前登录信息 + */ + fun getCurrentLoginInfo(): LoginResult? { + return if (isLoggedIn()) currentLoginResult else null + } + + /** + * 登录接口 + */ + fun login(callback: (Result) -> Unit) { + if (!isQQMusicInstalled()){ + callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) + return + } + val currentActivity = activity + // 先检查是否已经登录且未过期 + if (isLoggedIn()) { + Log.d(TAG, "用户已登录且未过期,直接返回登录信息") + ensureMusicManager(currentActivity!!) + callback(Result.success(currentLoginResult!!)) + return + } + + + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + Log.d(TAG, "开始新的登录流程") + loginManager = QQMusicLoginManager(currentActivity) + loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback { + override fun onSuccess(openId: String, openToken: String, expireTime: String) { + handler.post { + ensureMusicManager(currentActivity!!) + val result = LoginResult(openId, openToken, expireTime) + currentLoginResult = result + saveLoginInfo(result) + Log.d(TAG, "登录成功并已保存: $result") + callback(Result.success(result)) + } + } + + override fun onError(errorCode: String, errorMessage: String) { + handler.post { + Log.e(TAG, "登录失败: $errorCode - $errorMessage") + callback(Result.failure(Exception("$errorCode: $errorMessage"))) + } + } + }) + } + + /** + * 登录接口 + */ + fun autologin(callback: (Result) -> Unit) { + if (!isQQMusicInstalled()){ + callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) + return + } + val currentActivity = activity + + // 先检查是否已经登录且未过期 + if (isLoggedIn()) { + Log.d(TAG, "用户已登录且未过期,直接返回登录信息") + ensureMusicManager(currentActivity!!) + callback(Result.success(currentLoginResult!!)) + return + }else{ + callback(Result.failure(Exception("no logined"))) + return + } + } + + /** + * 登出 + */ + fun logout() { + currentLoginResult = null + clearSavedLoginInfo() + Log.d(TAG, "用户已登出,清除本地登录信息") + } + + /** + * 搜索歌曲(带自动登录检查和重试机制) + */ + fun searchSongs( + keyword: String, + searchType: Int = Data.SearchType.SEARCH_TYPE_SONG, + callback: (Result>>) -> Unit + ) { + if (!isQQMusicInstalled()){ + callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) + return + } + searchWithRetry(keyword, searchType, 0, callback) + } + + /** + * 带重试的搜索实现 + */ + private fun searchWithRetry( + keyword: String, + searchType: Int, + retryCount: Int, + callback: (Result>>) -> Unit + ) { + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + // 检查登录状态 + if (!isLoggedIn()) { + Log.d(TAG, "搜索前检测到未登录,开始自动登录") + login { loginResult -> + if (loginResult.isSuccess) { + Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...") + handler.postDelayed({ + Log.d(TAG, "等待完成,开始搜索") + performSearchWithTimeout(keyword, searchType, retryCount, callback) + }, LOGIN_WAIT_DELAY_MS) + } else { + callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) + } + } + } else { + performSearchWithTimeout(keyword, searchType, retryCount, callback) + } + } + + /** + * 执行带超时的搜索操作 + */ + private fun performSearchWithTimeout( + keyword: String, + searchType: Int, + retryCount: Int, + callback: (Result>>) -> Unit + ) { + val currentActivity = activity ?: return + val searchKey = "${keyword}_${System.currentTimeMillis()}" + var completed = false + + Log.d(TAG, "开始搜索: $keyword (第${retryCount + 1}次)") + + // 设置超时 + val timeoutRunnable = Runnable { + if (!completed) { + completed = true + searchTimeouts.remove(searchKey) + Log.w(TAG, "搜索超时: $keyword") + + if (retryCount < MAX_RETRY_COUNT) { + Log.d(TAG, "准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT") + handler.postDelayed({ + searchWithRetry(keyword, searchType, retryCount + 1, callback) + }, RETRY_DELAY_MS) + } else { + callback(Result.failure(Exception("搜索超时,已重试${MAX_RETRY_COUNT}次"))) + } + } + } + + searchTimeouts[searchKey] = timeoutRunnable + handler.postDelayed(timeoutRunnable, SEARCH_TIMEOUT_MS) + + ensureMusicManager(currentActivity) + musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback { + override fun onSuccess(songs: List>) { + handler.post { + if (!completed) { + completed = true + searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) } + Log.d(TAG, "搜索成功: 找到 ${songs.size} 首歌曲") + callback(Result.success(songs)) + } + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + if (!completed) { + completed = true + searchTimeouts.remove(searchKey)?.let { handler.removeCallbacks(it) } + Log.e(TAG, "搜索失败: code=$errorCode, message=$errorMessage") + + // 如果是登录相关错误,清除本地登录信息 + if (errorMessage.contains("登录") || errorMessage.contains("授权")) { + Log.d(TAG, "检测到登录相关错误,清除本地登录信息") + logout() + } + + // 网络相关错误才重试 + if (retryCount < MAX_RETRY_COUNT && + (errorMessage.contains("网络") || errorMessage.contains("超时") || + errorMessage.contains("连接") || errorCode == -1)) { + Log.d(TAG, "网络错误,准备重试: ${retryCount + 1}/$MAX_RETRY_COUNT") + handler.postDelayed({ + searchWithRetry(keyword, searchType, retryCount + 1, callback) + }, RETRY_DELAY_MS) + } else { + callback(Result.failure(Exception("搜索失败: $errorMessage"))) + } + } + } + } + }) + } + + /** + * 播放歌曲列表(带自动登录检查) + */ + fun playSongs( + songMids: List, + index: Int = 0, + callback: (Result) -> Unit + ) { + if (!isQQMusicInstalled()){ + callback(Result.failure(Exception("未安装QQ音乐软件,请先安装QQ音乐软件"))) + return + } + playWithRetry(songMids, index, callback) + } + + /** + * 带自动登录检查的播放实现 + */ + private fun playWithRetry( + songMids: List, + index: Int, + callback: (Result) -> Unit + ) { + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + // 检查登录状态 + if (!isLoggedIn()) { + Log.d(TAG, "播放前检测到未登录,开始自动登录") + login { loginResult -> + if (loginResult.isSuccess) { + Log.d(TAG, "自动登录成功,等待QQ音乐服务连接...") + handler.postDelayed({ + Log.d(TAG, "等待完成,开始播放") + performPlaySongs(songMids, index, callback) + }, LOGIN_WAIT_DELAY_MS) + } else { + callback(Result.failure(Exception("自动登录失败: ${loginResult.exceptionOrNull()?.message}"))) + } + } + } else { + performPlaySongs(songMids, index, callback) + } + } + + /** + * 执行播放歌曲列表操作 + */ + private fun performPlaySongs( + songMids: List, + index: Int, + callback: (Result) -> Unit + ) { + val currentActivity = activity ?: return + + ensureMusicManager(currentActivity) + musicManager?.playSongs(songMids, index, 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, "播放失败: $errorCode $errorMessage") + if (errorCode == -1){ + musicManager?.bindQQMusicApiService() + callback(Result.failure(Exception("播放失败: $errorMessage"))) + } else if (errorCode == 1030) { + callback(Result.success(Unit)) + }else{ + callback(Result.failure(Exception("播放失败: $errorMessage"))) + } + } + } + }) + } + + /** + * 播放音乐 + */ + fun playMusic(callback: (Result) -> Unit) { + executePlaybackAction("播放") { musicManager, playCallback -> + musicManager.playMusic(playCallback) + }(callback) + } + + /** + * 暂停音乐 + */ + fun pauseMusic(callback: (Result) -> Unit) { + executePlaybackAction("暂停") { musicManager, playCallback -> + musicManager.pauseMusic(playCallback) + }(callback) + } + + /** + * 恢复播放 + */ + fun resumeMusic(callback: (Result) -> Unit) { + executePlaybackAction("恢复播放") { musicManager, playCallback -> + musicManager.resumeMusic(playCallback) + }(callback) + } + + /** + * 停止音乐 + */ + fun stopMusic(callback: (Result) -> Unit) { + executePlaybackAction("停止") { musicManager, playCallback -> + musicManager.stopMusic(playCallback) + }(callback) + } + + /** + * 下一首 + */ + fun skipToNext(callback: (Result) -> Unit) { + executePlaybackAction("下一首") { musicManager, playCallback -> + musicManager.skipToNext(playCallback) + }(callback) + } + + /** + * 上一首 + */ + fun skipToPrevious(callback: (Result) -> Unit) { + executePlaybackAction("上一首") { musicManager, playCallback -> + musicManager.skipToPrevious(playCallback) + }(callback) + } + + /** + * 获取播放状态 + */ + fun getPlaybackState(callback: (Result>) -> Unit) { + executeInfoAction("获取播放状态") { musicManager, infoCallback -> + musicManager.getPlaybackState(infoCallback) + }(callback) + } + + /** + * 获取当前歌曲信息 + */ + fun getCurrentSong(callback: (Result>) -> Unit) { + executeInfoAction("获取当前歌曲") { musicManager, infoCallback -> + musicManager.getCurrentSong(infoCallback) + }(callback) + } + + /** + * 获取播放时间 + */ + fun getPlayTime(callback: (Result) -> Unit) { + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + ensureMusicManager(currentActivity) + musicManager?.getPlayTime(object : QQMusicManager.InfoCallback { + override fun onSuccess(data: Map) { + handler.post { + val totalTime = data["totalTime"] as? Long ?: 0L + val currentTime = data["currentTime"] as? Long ?: 0L + val playTimeInfo = PlayTimeInfo(totalTime, currentTime) + Log.d(TAG, "获取播放时间成功: $playTimeInfo") + callback(Result.success(playTimeInfo)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "获取播放时间失败: $errorMessage") + callback(Result.failure(Exception("获取播放时间失败: $errorMessage"))) + } + } + }) + } + + /** + * 同步当前播放信息 - 统一接口 + */ + fun syncCurrentPlayInfo(callback: (Result>) -> Unit) { + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return + } + + ensureMusicManager(currentActivity) + musicManager?.syncCurrentPlayInfo(object : QQMusicManager.InfoCallback { + override fun onSuccess(data: Map) { + handler.post { + Log.d(TAG, "同步播放信息成功") + callback(Result.success(data)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "同步播放信息失败: $errorMessage") + callback(Result.failure(Exception("同步播放信息失败: $errorMessage"))) + } + } + }) + } + + /** + * 清理资源 + */ + fun cleanup() { + // 清理搜索超时任务 + searchTimeouts.values.forEach { handler.removeCallbacks(it) } + searchTimeouts.clear() + + // 清理管理器 + loginManager?.cleanup() + loginManager = null + musicManager?.cleanup() + musicManager = null + activity = 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(activity: Activity) { + if (musicManager == null) { + musicManager = QQMusicManager(activity) + } + } + + private fun executePlaybackAction( + actionName: String, + action: (QQMusicManager, QQMusicManager.PlayCallback) -> Unit + ): (callback: (Result) -> Unit) -> Unit { + return lambda@{ callback -> + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return@lambda + } + + ensureMusicManager(currentActivity) + action(musicManager!!, object : QQMusicManager.PlayCallback { + override fun onSuccess() { + handler.post { + Log.d(TAG, "${actionName}成功") + callback(Result.success(Unit)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "${actionName}失败: $errorMessage") + callback(Result.failure(Exception("${actionName}失败: $errorMessage"))) + } + } + }) + } + } + + private fun executeInfoAction( + actionName: String, + action: (QQMusicManager, QQMusicManager.InfoCallback) -> Unit + ): (callback: (Result>) -> Unit) -> Unit { + return lambda@{ callback -> + val currentActivity = activity + if (currentActivity == null) { + callback(Result.failure(Exception("Activity not available"))) + return@lambda + } + + ensureMusicManager(currentActivity) + action(musicManager!!, object : QQMusicManager.InfoCallback { + override fun onSuccess(data: Map) { + handler.post { + Log.d(TAG, "${actionName}成功") + callback(Result.success(data)) + } + } + + override fun onError(errorCode: Int, errorMessage: String) { + handler.post { + Log.e(TAG, "${actionName}失败: $errorMessage") + callback(Result.failure(Exception("${actionName}失败: $errorMessage"))) + } + } + }) + } + } + + // 数据类定义 + data class LoginResult( + val openId: String, + val openToken: String, + val expireTime: String + ) + + data class PlayTimeInfo( + val totalTime: Long, + val currentTime: Long + ) +} diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt new file mode 100644 index 000000000..9c9cd8e26 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt @@ -0,0 +1,245 @@ +package com.yunqiinnovation.qq_music + +import android.app.Activity +import android.util.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import com.tencent.qqmusic.third.api.contract.* + +/** QqMusicPlugin */ +class QqMusicPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { + private lateinit var channel: MethodChannel + private var activity: Activity? = null + private val qqMusicSingleton = QQMusicSingleton.getInstance() + private var isRegistered = false + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + if (isRegistered) { + Log.w("QqMusicPlugin", "Plugin already registered, skipping") + return + } + + Log.d("QqMusicPlugin", "onAttachedToEngine called") + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music") + channel.setMethodCallHandler(this) + isRegistered = true + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + Log.d("QqMusicPlugin", "onDetachedFromEngine called") + if (::channel.isInitialized) { + channel.setMethodCallHandler(null) + } + isRegistered = false + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + Log.d("QqMusicPlugin", "onAttachedToActivity called") + activity = binding.activity + println("QQ音乐 onAttachedToActivity") + qqMusicSingleton.initialize(activity!!) + Log.d("QqMusicPlugin", "qqMusicSingleton.initialize completed") + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + Log.d("QqMusicPlugin", "onReattachedToActivityForConfigChanges called") + activity = binding.activity + // qqMusicSingleton.initialize(activity!!) + } + + override fun onDetachedFromActivityForConfigChanges() { + Log.d("QqMusicPlugin", "onDetachedFromActivityForConfigChanges called") + activity = null + } + + override fun onDetachedFromActivity() { + Log.d("QqMusicPlugin", "onDetachedFromActivity called") + activity = null + qqMusicSingleton.cleanup() + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "login" -> { + qqMusicSingleton.autologin { kotlinResult -> + kotlinResult.fold( + onSuccess = { loginResult -> + result.success(mapOf( + "success" to true, + "openId" to loginResult.openId, + "openToken" to loginResult.openToken, + "expireTime" to loginResult.expireTime + )) + }, + onFailure = { error -> + result.error("LOGIN_FAILED", error.message, null) + } + ) + } + } + "searchSongs" -> { + val keyword = call.argument("keyword") + val searchType = call.argument("searchType") ?: Data.SearchType.SEARCH_TYPE_SONG + + if (keyword == null) { + result.error("INVALID_ARGUMENT", "keyword is required", null) + return + } + + qqMusicSingleton.searchSongs(keyword, searchType) { kotlinResult -> + kotlinResult.fold( + onSuccess = { songs -> + result.success(mapOf( + "success" to true, + "songs" to songs + )) + }, + onFailure = { error -> + result.error("SEARCH_FAILED", error.message, null) + } + ) + } + } + "playSongs" -> { + val songMids = call.argument>("songMids") + val index = call.argument("index") ?: 0 + + if (songMids == null || songMids.isEmpty()) { + result.error("INVALID_ARGUMENT", "songMids is required", null) + return + } + + qqMusicSingleton.playSongs(songMids, index) { kotlinResult -> + kotlinResult.fold( + onSuccess = { + result.success(mapOf("success" to true)) + }, + onFailure = { error -> + result.error("PLAY_FAILED", error.message, null) + } + ) + } + } + "playMusic" -> { + qqMusicSingleton.playMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("PLAY_FAILED", error.message, null) } + ) + } + } + "pauseMusic" -> { + qqMusicSingleton.pauseMusic { kotlinResult -> + kotlinResult.fold( + onSuccess = { result.success(mapOf("success" to true)) }, + onFailure = { error -> result.error("PAUSE_FAILED", error.message, null) } + ) + } + } + "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) + } + ) + } + } + "getPlayTime" -> { + qqMusicSingleton.getPlayTime { kotlinResult -> + kotlinResult.fold( + onSuccess = { playTimeInfo -> + result.success(mapOf( + "success" to true, + "totalTime" to playTimeInfo.totalTime, + "currentTime" to playTimeInfo.currentTime + )) + }, + onFailure = { error -> + result.error("GET_TIME_FAILED", error.message, null) + } + ) + } + } + "syncCurrentPlayInfo" -> { + qqMusicSingleton.syncCurrentPlayInfo { kotlinResult -> + kotlinResult.fold( + onSuccess = { data -> + result.success(mapOf( + "success" to true, + "data" to data + )) + }, + onFailure = { error -> + result.error("SYNC_FAILED", error.message, null) + } + ) + } + } + "getPlatformVersion" -> { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } + else -> { + result.notImplemented() + } + } + } +} diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java new file mode 100644 index 000000000..d7f374394 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java @@ -0,0 +1,64 @@ +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, "voitrans://"); + String sourceString = signJson.toString(); + + //2. 使用Q音公钥加密(随机数+签名) + byte[] encryptData = RSAUtils.encryptByPublicKey(sourceString.getBytes(), QQMusicPublicKey); + if (encryptData == null) { + return null; + } + return Base64.encodeToString(encryptData, Base64.DEFAULT); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + public static String decryptQQMEncryptString(String qmEncryptString) { + try { + byte[] qmEncryptData = Base64.decode(qmEncryptString, Base64.DEFAULT); + //7.使用App私钥解密 - 修改这里,使用Base64格式的私钥 + byte[] decryptData = RSAUtils.decryptByPrivateKey(qmEncryptData, Config.getBase64PrivateKey()); + if (decryptData != null) { + return new String(decryptData); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + public static boolean checkQMSign(String sign, String nonce) { + try { + return RSAUtils.verify(nonce.getBytes(), QQMusicPublicKey, sign); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } +} diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java new file mode 100644 index 000000000..0a0704fe5 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java @@ -0,0 +1,314 @@ +package com.yunqiinnovation.qq_music.openid; + +import android.util.Base64; + +import java.io.ByteArrayOutputStream; +import java.security.Key; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.Signature; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.HashMap; +import java.util.Map; + +import javax.crypto.Cipher; + +public class RSAUtils { + /** + * 加密算法RSA + */ + public static final String KEY_ALGORITHM = "RSA"; + + /** + * 签名算法 + */ + public static final String SIGNATURE_ALGORITHM = "SHA256withRSA";//"MD5withRSA"; + + /** + * 获取公钥的key + */ + private static final String PUBLIC_KEY = "RSAPublicKey"; + + /** + * 获取私钥的key + */ + private static final String PRIVATE_KEY = "RSAPrivateKey"; + + private static final String CIHPER_KEY = "RSA/ECB/PKCS1Padding";// + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + /** + * 生成密钥对(公钥和私钥) + * + * @return + * @throws Exception + */ + public static Map genKeyPair() throws Exception { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGen.initialize(1024); + KeyPair keyPair = keyPairGen.generateKeyPair(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + Map keyMap = new HashMap<>(2); + keyMap.put(PUBLIC_KEY, publicKey); + keyMap.put(PRIVATE_KEY, privateKey); + return keyMap; + } + + /** + * 用私钥对信息生成数字签名 + * + * @param data 已加密数据 + * @param privateKey 私钥(BASE64编码) + * @return + * @throws Exception + */ + public static String sign(byte[] data, String privateKey) throws Exception { + + byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initSign(privateK); + signature.update(data); + return Base64.encodeToString(signature.sign(), Base64.DEFAULT); + } + + /** + * 校验数字签名 + * + * @param data 已加密数据 + * @param publicKey 公钥(BASE64编码) + * @param sign 数字签名 + * @return + * @throws Exception + */ + public static boolean verify(byte[] data, String publicKey, String sign) + throws Exception { + byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PublicKey publicK = keyFactory.generatePublic(keySpec); + Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM); + signature.initVerify(publicK); + signature.update(data); + return signature.verify(Base64.decode(sign, Base64.DEFAULT)); + } + + /** + * 私钥解密 + * + * @param encryptedData 已加密数据 + * @param privateKey 私钥(BASE64编码) + * @return + * @throws Exception + */ + public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) + throws Exception { + byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key privateK = keyFactory.generatePrivate(pkcs8KeySpec); +// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + Cipher cipher = Cipher.getInstance(CIHPER_KEY); + + cipher.init(Cipher.DECRYPT_MODE, privateK); + int inputLen = encryptedData.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); + } else { + cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_DECRYPT_BLOCK; + } + byte[] decryptedData = out.toByteArray(); + out.close(); + return decryptedData; + } + + public static String testDecryptByJavaPrivKey(String privkey_from_java, String encryptedString) throws Exception { + //加解密类 + Cipher cipher = Cipher.getInstance("RSA");//Cipher.getInstance("RSA/ECB/PKCS1Padding"); + // G0语言通过java产生的公钥加密得到的加密后的字符串 + byte[] enBytes = Base64.decode(encryptedString, Base64.DEFAULT); + //通过密钥字符串得到密钥 + + PrivateKey privateKey = testGetPrivateKey(privkey_from_java); + + + //解密 + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] deBytes = cipher.doFinal(enBytes); + return new String(deBytes); + + } + + public static PrivateKey testGetPrivateKey(String key) throws Exception { + byte[] keyBytes; + keyBytes = Base64.decode(key, Base64.DEFAULT); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + return keyFactory.generatePrivate(keySpec); + } + + /** + * 公钥解密 + * + * @param encryptedData 已加密数据 + * @param publicKey 公钥(BASE64编码) + * @return + * @throws Exception + */ + public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey) + throws Exception { + byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key publicK = keyFactory.generatePublic(x509KeySpec); +// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + Cipher cipher = Cipher.getInstance(CIHPER_KEY); + cipher.init(Cipher.DECRYPT_MODE, publicK); + int inputLen = encryptedData.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); + } else { + cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_DECRYPT_BLOCK; + } + byte[] decryptedData = out.toByteArray(); + out.close(); + return decryptedData; + } + + /** + * 公钥加密 + * + * @param data 源数据 + * @param publicKey 公钥(BASE64编码) + * @return + * @throws Exception + */ + public static byte[] encryptByPublicKey(byte[] data, String publicKey) + throws Exception { + byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key publicK = keyFactory.generatePublic(x509KeySpec); + // 对数据加密 +// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + Cipher cipher = Cipher.getInstance(CIHPER_KEY); + cipher.init(Cipher.ENCRYPT_MODE, publicK); + int inputLen = data.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK); + } else { + cache = cipher.doFinal(data, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = out.toByteArray(); + out.close(); + return encryptedData; + } + + /** + * 私钥加密 + * + * @param data 源数据 + * @param privateKey 私钥(BASE64编码) + * @return + * @throws Exception + */ + public static byte[] encryptByPrivateKey(byte[] data, String privateKey) + throws Exception { + byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key privateK = keyFactory.generatePrivate(pkcs8KeySpec); +// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm()); + Cipher cipher = Cipher.getInstance(CIHPER_KEY); + cipher.init(Cipher.ENCRYPT_MODE, privateK); + int inputLen = data.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK); + } else { + cache = cipher.doFinal(data, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = out.toByteArray(); + out.close(); + return encryptedData; + } + + /** + * 获取私钥 + * + * @param keyMap 密钥对 + * @return + */ + public static String getPrivateKey(Map keyMap) { + Key key = (Key) keyMap.get(PRIVATE_KEY); + return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); + } + + /** + * 获取公钥 + * + * @param keyMap 密钥对 + * @return + */ + public static String getPublicKey(Map keyMap) { + Key key = (Key) keyMap.get(PUBLIC_KEY); + return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT); + } +} diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt new file mode 100644 index 000000000..7a8a868e0 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt @@ -0,0 +1,253 @@ +package com.yunqiinnovation.qq_music.pcm + +import android.media.AudioFormat +import android.media.AudioManager +import android.media.AudioRecord +import android.media.AudioTrack +import android.media.AudioTrack.* +import android.os.Build +import android.os.ParcelFileDescriptor +import android.os.Process +import android.util.Log +import com.yunqiinnovation.qq_music.util.BitConverter +import java.io.DataInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStream +import java.util.concurrent.ConcurrentLinkedQueue + + +class AudioTrackManager { + companion object { + private const val TAG = "AudioTrackManager" + private const val LOG_LOOP = true + + //音频流类型 + private const val mStreamType = AudioManager.STREAM_MUSIC + + //指定采样率 (MediaRecoder 的采样率通常是8000Hz AAC的通常是44100Hz。 设置采样率为44100,目前为常用的采样率,官方文档表示这个值可以兼容所有的设置) + private const val mSampleRateInHz = 44100 + + //指定捕获音频的声道数目。在AudioFormat类中指定用于此的常量 + private const val mChannelConfig = AudioFormat.CHANNEL_IN_STEREO //STEREO立体声 + + //指定音频量化位数 ,在AudioFormaat类中指定了以下各种可能的常量。通常我们选择ENCODING_PCM_16BIT和ENCODING_PCM_8BIT PCM代表的是脉冲编码调制,它实际上是原始音频样本。 + //因此可以设置每个样本的分辨率为16位或者8位,16位将占用更多的空间和处理能力,表示的音频也更加接近真实。 + private const val mAudioFormat = AudioFormat.ENCODING_PCM_FLOAT + + //STREAM的意思是由用户在应用程序通过write方式把数据一次一次得写到audiotrack中。这个和我们在socket中发送数据一样, + // 应用层从某个地方获取数据,例如通过编解码得到PCM数据,然后write到audiotrack。 + private const val mMode = AudioTrack.MODE_STREAM + } + + private var mAudioTrack: AudioTrack? = null + + private var mInputStream: InputStream? = null //播放的数据流 + private var mPlayerThread: Thread? = null + + //指定缓冲区大小。调用AudioRecord类的getMinBufferSize方法可以获得。 + private var mMinBufferSize = 0 + + var printMessageCallback: ((String) -> Unit)? = null + var enterForeground: ((Boolean) -> Unit)? = null + + var isFloatPcm = true + + fun initData(sampleRateInHz: Int = mSampleRateInHz, channelConfig: Int = mChannelConfig, audioFormat: Int = mAudioFormat) { + Log.d(TAG, "[initData】$sampleRateInHz, $channelConfig, $audioFormat") + //根据采样率,采样精度,单双声道来得到frame的大小。 + mMinBufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat) //计算最小缓冲区 + //注意,按照数字音频的知识,这个算出来的是一秒钟buffer的大小。 + //创建AudioTrack + mAudioTrack = AudioTrack(mStreamType, sampleRateInHz, channelConfig, audioFormat, mMinBufferSize, mMode) + isFloatPcm = audioFormat == AudioFormat.ENCODING_PCM_FLOAT + play() + } + + /** + * 销毁线程方法 + */ + private fun destroyThread() { + try { + if (null != mPlayerThread && Thread.State.RUNNABLE == mPlayerThread!!.state) { + try { + Thread.sleep(500) + mPlayerThread!!.interrupt() + } catch (e: Exception) { + mPlayerThread = null + } + } + mPlayerThread = null + } catch (e: Exception) { + e.printStackTrace() + } finally { + mPlayerThread = null + } + } + + /** + * 启动播放线程 + */ + private fun startThread() { + destroyThread() + if (mPlayerThread == null) { + mPlayerThread = Thread(streamPlayerRunnable) + mPlayerThread!!.start() + } + } + + private val streamPlayerRunnable = Runnable { + try { + //设置线程的优先级 + Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO) + Log.d(TAG, "streamPlayerRunnable") + + val tempBuffer = ByteArray(mMinBufferSize * 4) + var readCount = 0 + while (true) { + if (LOG_LOOP) { + //Log.d(TAG, "while true") + } + if (mInputStream!!.available() <= 0) { + if (LOG_LOOP) { + //Log.d(TAG, "sleep") + } + printMessageCallback?.invoke("Play thread sleep \n PlayState:${mAudioTrack?.playState?.playStateToString()}") + Thread.sleep(500) + continue + } + + readCount = mInputStream!!.read(tempBuffer) + if (LOG_LOOP) { + Log.d(TAG, "readCount:$readCount") + } + if (readCount == AudioTrack.ERROR_INVALID_OPERATION || readCount == AudioTrack.ERROR_BAD_VALUE) { + continue + } + if (readCount != 0 && readCount != -1) { + //判断AudioTrack未初始化,停止播放的时候释放了,状态就为STATE_UNINITIALIZED + if (mAudioTrack!!.state == AudioTrack.STATE_UNINITIALIZED) { + initData() + } + + printMessageCallback?.invoke("AudioTrack write $readCount Bytes\n PlayState:${mAudioTrack?.playState?.playStateToString()}") + + if (isFloatPcm) { + val size = readCount / 4 + val audioData = FloatArray(size) + for (i in 0 until size) { + // 框架地址:https://github.com/smartdevicelink/sdl_java_suite/blob/master/base/src/main/java/com/smartdevicelink/util/BitConverter.java + val bitInt = BitConverter.intFromByteArray(tempBuffer, i * 4) + val fl = java.lang.Float.intBitsToFloat(bitInt) + audioData[i] = fl + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + val ret = mAudioTrack?.write(audioData, 0, audioData.size, WRITE_BLOCKING) + Log.d(TAG, "ret=$ret") + } + } else { + mAudioTrack!!.write(tempBuffer, 0, readCount) + } + + } + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + fun Int.playStateToString(): String { + return when (this) { + PLAYSTATE_STOPPED -> { + "PLAYSTATE_STOPPED" + } + PLAYSTATE_PAUSED -> { + "PLAYSTATE_PAUSED" + } + PLAYSTATE_PLAYING -> { + "PLAYSTATE_PLAYING" + } + else -> { + "unknown" + } + } + } + + /** + * 播放文件 + * + * @param path + * @throws Exception + */ + @Throws(Exception::class) + private fun setPath(path: String) { + val file = File(path) + mInputStream = DataInputStream(FileInputStream(file)) + } + + /** + * 启动播放 + * + * @param path + */ + fun startPlay(path: String) { + try { + //AudioTrack未初始化 + setPath(path) + startThread() + } catch (e: Exception) { + Log.d(TAG, "startPlay: Exception$e") + } + } + + fun startPlay(inputStream: InputStream?) { + initData() + try { + mInputStream = DataInputStream(inputStream) + startThread() + } catch (e: Exception) { + Log.d(TAG, "startPlay: Exception$e") + } + } + + fun startPlayByFileDescriptor(fileDescriptor: ParcelFileDescriptor) { + mInputStream = ParcelFileDescriptor.AutoCloseInputStream(fileDescriptor) + startThread() + } + + /** + * 停止播放 + */ + fun stopPlay() { + try { + destroyThread() //销毁线程 + if (mAudioTrack != null) { + if (mAudioTrack!!.state == AudioRecord.STATE_INITIALIZED) { //初始化成功 + mAudioTrack!!.stop() //停止播放 + } + if (mAudioTrack != null) { + mAudioTrack!!.release() //释放audioTrack资源 + } + mAudioTrack = null + } + if (mInputStream != null) { + mInputStream!!.close() //关闭数据输入流 + } + } catch (e: Exception) { + e.printStackTrace() + } + enterForeground?.invoke(false) + } + + + fun pause() { + mAudioTrack?.pause() + enterForeground?.invoke(false) + } + + fun play() { + mAudioTrack?.play() + enterForeground?.invoke(true) + } + +} \ No newline at end of file diff --git a/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PcmExampleActivityNew.kt b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PcmExampleActivityNew.kt new file mode 100644 index 000000000..fcff96851 --- /dev/null +++ b/local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PcmExampleActivityNew.kt @@ -0,0 +1,79 @@ +package com.yunqiinnovation.qq_music.pcm + +import android.content.* +import android.os.Bundle +import android.os.IBinder +import androidx.appcompat.app.AppCompatActivity +import android.widget.Button +import android.widget.TextView +import android.widget.Toast +import com.yunqiinnovation.qq_music.* +import com.yunqiinnovation.qq_music.R + + +class PcmExampleActivityNew : AppCompatActivity() { + companion object { + const val TAG = "PcmExampleActivity" + } + + private lateinit var informationTextView: TextView + private var playService: IPlayService? = null + + private val conn = object : ServiceConnection { + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + playService = IPlayService.Stub.asInterface(service) + playService?.setPrintMessageCallback(object :IPrint.Stub(){ + override fun print(msg: String?) { + runOnUiThread { + informationTextView.text = msg + } + } + }) + } + + override fun onServiceDisconnected(name: ComponentName?) { + playService?.setPrintMessageCallback(null) + playService = null + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_pcm_example) + + informationTextView = findViewById(R.id.tv_information) + + val intent = Intent(this, PlayerService::class.java) + startService(intent) + bindService(intent, conn, BIND_AUTO_CREATE) + + findViewById