Browse Source

上传qq音乐

newdev_shunjiawei
liwei1dao 1 year ago
parent
commit
eb4fb1347c
  1. 1
      android/app/build.gradle.kts
  2. 6
      android/settings.gradle.kts
  3. 4
      lib/core/bindings/initial_binding.dart
  4. 157
      lib/data/services/qq_musoc.dart
  5. 9
      lib/modules/qqmusic/bindings/qqmusic_binding.dart
  6. 326
      lib/modules/qqmusic/controllers/qqmusic_controller.dart
  7. 49
      lib/modules/qqmusic/models/song_model.dart
  8. 242
      lib/modules/qqmusic/views/qqmusic_view.dart
  9. 4
      lib/modules/splash/controllers/splash_controller.dart
  10. 8
      lib/routes/app_pages.dart
  11. 2
      lib/routes/app_routes.dart
  12. 33
      local_plugins/qq_music/.gitignore
  13. 33
      local_plugins/qq_music/.metadata
  14. 3
      local_plugins/qq_music/CHANGELOG.md
  15. 1
      local_plugins/qq_music/LICENSE
  16. 15
      local_plugins/qq_music/README.md
  17. 4
      local_plugins/qq_music/analysis_options.yaml
  18. 9
      local_plugins/qq_music/android/.gitignore
  19. 79
      local_plugins/qq_music/android/build.gradle
  20. BIN
      local_plugins/qq_music/android/libs/qqmusic-innovation-aidl-api-sdk-1.0.26.aar
  21. 1
      local_plugins/qq_music/android/settings.gradle
  22. 9
      local_plugins/qq_music/android/src/main/AndroidManifest.xml
  23. 15
      local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPlayService.aidl
  24. 8
      local_plugins/qq_music/android/src/main/aidl/com/yunqiinnovation/qq_music/IPrint.aidl
  25. 361
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/ApiSample.kt
  26. 53
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/Config.java
  27. 239
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt
  28. 495
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt
  29. 393
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt
  30. 64
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/OpenIDHelper.java
  31. 314
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/openid/RSAUtils.java
  32. 253
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/AudioTrackManager.kt
  33. 79
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PcmExampleActivityNew.kt
  34. 383
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt
  35. 17
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/BitConverter.java
  36. 177
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt
  37. 105
      local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QQMusicApiWrapper.kt
  38. 34
      local_plugins/qq_music/android/src/main/res/drawable-v24/ic_launcher_foreground.xml
  39. 171
      local_plugins/qq_music/android/src/main/res/drawable/ic_launcher_background.xml
  40. 8
      local_plugins/qq_music/android/src/main/res/layout/activity_callback.xml
  41. 17
      local_plugins/qq_music/android/src/main/res/layout/activity_example_list.xml
  42. 20
      local_plugins/qq_music/android/src/main/res/layout/activity_login_example.xml
  43. 170
      local_plugins/qq_music/android/src/main/res/layout/activity_main.xml
  44. 48
      local_plugins/qq_music/android/src/main/res/layout/activity_pcm_example.xml
  45. 220
      local_plugins/qq_music/android/src/main/res/layout/activity_visual.xml
  46. 21
      local_plugins/qq_music/android/src/main/res/layout/folder_list_view_item.xml
  47. 45
      local_plugins/qq_music/android/src/main/res/layout/song_list_view_item.xml
  48. 24
      local_plugins/qq_music/android/src/main/res/layout/view_popview_more.xml
  49. 14
      local_plugins/qq_music/android/src/main/res/menu/toolbar_menu.xml
  50. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-hdpi/ic_launcher.png
  51. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-hdpi/ic_launcher_round.png
  52. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-hdpi/mini_album_default.png
  53. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-mdpi/ic_launcher.png
  54. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-mdpi/ic_launcher_round.png
  55. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xhdpi/ic_launcher.png
  56. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png
  57. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xxhdpi/ic_launcher.png
  58. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
  59. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  60. BIN
      local_plugins/qq_music/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
  61. 18
      local_plugins/qq_music/android/src/main/res/values/array.xml
  62. 7
      local_plugins/qq_music/android/src/main/res/values/colors.xml
  63. 4
      local_plugins/qq_music/android/src/main/res/values/dimens.xml
  64. 12
      local_plugins/qq_music/android/src/main/res/values/strings.xml
  65. 20
      local_plugins/qq_music/android/src/main/res/values/styles.xml
  66. 38
      local_plugins/qq_music/ios/.gitignore
  67. 29
      local_plugins/qq_music/ios/qq_music.podspec
  68. 32
      local_plugins/qq_music/ios/qq_music/Package.swift
  69. 14
      local_plugins/qq_music/ios/qq_music/Sources/qq_music/PrivacyInfo.xcprivacy
  70. 19
      local_plugins/qq_music/ios/qq_music/Sources/qq_music/QqMusicPlugin.swift
  71. 279
      local_plugins/qq_music/lib/qq_music.dart
  72. 28
      local_plugins/qq_music/pubspec.yaml
  73. 2
      pubspec.yaml

1
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")) // 添加这行
}
}

6
android/settings.gradle.kts

@ -41,9 +41,10 @@ include(":agent_service")
include(":speech")
include(":chat_api")
include(":bytedance_speech")
include(":qq_music") // 添加这行
include(":jl_opus")
// 设置azure_speech项目的路径
// 设置项目路径
project(":azure_speech").projectDir = file("../local_plugins/azure_speech/android")
project(":chat_storage").projectDir = file("../local_plugins/chat_storage/android")
project(":jieli").projectDir = file("../local_plugins/jieli/android")
@ -54,4 +55,5 @@ project(":agent_service").projectDir = file("../local_plugins/agent_service/andr
project(":speech").projectDir = file("../local_plugins/speech/android")
project(":chat_api").projectDir = file("../local_plugins/chat_api/android")
project(":bytedance_speech").projectDir = file("../local_plugins/bytedance_speech/android")
project(":qq_music").projectDir = file("../local_plugins/qq_music/android") // 添加这行
project(":jl_opus").projectDir = file("../local_plugins/jl_opus/android")

4
lib/core/bindings/initial_binding.dart

@ -1,3 +1,5 @@
import 'package:voitrans/data/services/qq_musoc.dart';
import '../../../data/services/user_portrait.dart';
import '../../../data/services/location_manager.dart';
@ -63,6 +65,8 @@ class InitialBinding extends Bindings {
// 会议助手音频上传
Get.lazyPut<UserportraitManager>(() => UserportraitManager(), fenix: true);
// qq音乐服务
Get.lazyPut<QqMusocService>(() => QqMusocService(), fenix: true);
// 杰理蓝牙服务

157
lib/data/services/qq_musoc.dart

@ -0,0 +1,157 @@
import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart';
/// QQ音乐服务适配器
class QqMusocService extends GetxService {
static QqMusocService get to => Get.find<QqMusocService>();
var isInitialized = false;
// 音乐服务实例
final QqMusic _qqMusic = QqMusic();
/// 初始化
@override
void onInit() {
super.onInit();
}
/// 登录QQ音乐
Future<Map<String, dynamic>> login() async {
try {
// 直接调用QQ音乐插件的登录方法
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, // SEARCH_TYPE_SONG
}) 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,
}) async {
try {
final result = await _qqMusic.playSongs(
songMids: songMids,
index: index,
);
return result;
} catch (e) {
return {'success': false, 'error': '播放失败: ${e.toString()}'};
}
}
/// 播放音乐
Future<Map<String, dynamic>> playMusic() async {
try {
final result = await _qqMusic.playMusic();
return result;
} catch (e) {
return {'success': false, 'error': '播放失败: ${e.toString()}'};
}
}
/// 暂停音乐
Future<Map<String, dynamic>> pauseMusic() async {
try {
final result = await _qqMusic.pauseMusic();
return result;
} catch (e) {
return {'success': false, 'error': '暂停失败: ${e.toString()}'};
}
}
/// 继续播放
Future<Map<String, dynamic>> resumeMusic() async {
try {
final result = await _qqMusic.resumeMusic();
return result;
} catch (e) {
return {'success': false, 'error': '继续播放失败: ${e.toString()}'};
}
}
/// 停止音乐
Future<Map<String, dynamic>> stopMusic() async {
try {
final result = await _qqMusic.stopMusic();
return result;
} catch (e) {
return {'success': false, 'error': '停止失败: ${e.toString()}'};
}
}
/// 下一首
Future<Map<String, dynamic>> skipToNext() async {
try {
final result = await _qqMusic.skipToNext();
return result;
} catch (e) {
return {'success': false, 'error': '下一首失败: ${e.toString()}'};
}
}
/// 上一首
Future<Map<String, dynamic>> skipToPrevious() async {
try {
final result = await _qqMusic.skipToPrevious();
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()}'};
}
}
}

9
lib/modules/qqmusic/bindings/qqmusic_binding.dart

@ -0,0 +1,9 @@
import 'package:get/get.dart';
import '../controllers/qqmusic_controller.dart';
class QQMusicBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<QQMusicController>(() => QQMusicController());
}
}

326
lib/modules/qqmusic/controllers/qqmusic_controller.dart

@ -0,0 +1,326 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart';
import 'package:voitrans/data/services/qq_musoc.dart';
import '../models/song_model.dart';
class QQMusicController extends GetxController {
final QqMusocService _qqMusic = Get.find<QqMusocService>();
// 登录状态
final isLoggedIn = false.obs;
final userInfo = Rxn<QQMusicLoginResult>();
// 搜索相关
final searchResults = <SongModel>[].obs;
final isSearching = false.obs;
final searchKeyword = ''.obs;
// 播放相关
final currentSong = Rxn<SongModel>();
final isPlaying = false.obs;
final currentPosition = 0.obs;
final totalDuration = 0.obs;
final playlist = <SongModel>[].obs;
final currentIndex = 0.obs;
@override
void onInit() {
super.onInit();
_checkLoginStatus();
_startPlaybackStateMonitor();
}
/// 检查登录状态
void _checkLoginStatus() {
// 这里可以检查本地存储的登录信息
// 暂时设为未登录状态
isLoggedIn.value = false;
}
/// QQ音乐登录
Future<void> login() async {
try {
print('开始QQ音乐登录...');
Get.dialog(
const Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
// 添加超时处理
final result = await _qqMusic.login().timeout(
const Duration(seconds: 30),
onTimeout: () {
print('QQ音乐登录超时');
return {'success': false, 'error': '登录超时,请重试'};
},
);
print('QQ音乐登录结果: $result');
Get.back(); // 关闭加载对话框
if (result['success'] == true) {
userInfo.value = QQMusicLoginResult.fromMap(result);
isLoggedIn.value = true;
print('QQ音乐登录成功: ${result['openId']}');
Get.snackbar('登录成功', '欢迎使用QQ音乐');
} else {
print('QQ音乐登录失败: ${result['error']}');
Get.snackbar('登录失败', result['error'] ?? '未知错误');
}
} catch (e) {
print('QQ音乐登录异常: $e');
Get.back();
Get.snackbar('登录失败', e.toString());
}
}
/// 退出登录
void logout() {
userInfo.value = null;
isLoggedIn.value = false;
currentSong.value = null;
isPlaying.value = false;
playlist.clear();
Get.snackbar('已退出', '已退出QQ音乐登录');
}
/// 搜索歌曲
Future<void> searchSongs(String keyword) async {
if (keyword.trim().isEmpty) return;
print('开始搜索歌曲: $keyword');
searchKeyword.value = keyword;
isSearching.value = true;
try {
// 检查登录状态
if (!isLoggedIn.value) {
print('用户未登录,无法搜索');
Get.snackbar('搜索失败', '请先登录QQ音乐');
return;
}
print('调用搜索接口...');
final result = await _qqMusic.searchSongs(keyword: keyword).timeout(
const Duration(seconds: 15),
onTimeout: () {
print('搜索超时');
return {'success': false, 'error': '搜索超时,请重试'};
},
);
print('搜索结果: $result');
if (result['success'] == true && result['songs'] != null) {
final List<dynamic> songsData = result['songs'];
print('找到 ${songsData.length} 首歌曲');
final songs = songsData.map((songData) {
return SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
}).toList();
searchResults.assignAll(songs);
print('搜索成功,更新UI');
} else {
print('搜索失败: ${result['error']}');
Get.snackbar('搜索失败', result['error'] ?? '搜索结果为空');
searchResults.clear();
}
} catch (e) {
print('搜索异常: $e');
Get.snackbar('搜索失败', e.toString());
searchResults.clear();
} finally {
print('搜索完成,重置状态');
isSearching.value = false;
}
}
/// 播放歌曲
Future<void> playSong(SongModel song, {List<SongModel>? songList}) async {
try {
currentSong.value = song;
if (songList != null) {
playlist.assignAll(songList);
currentIndex.value = songList.indexOf(song);
} else {
playlist.assignAll([song]);
currentIndex.value = 0;
}
// 获取歌曲MID列表
final songMids = playlist.map((s) => s.mid).toList();
final playIndex = currentIndex.value;
// 调用QQ音乐播放接口
final result = await _qqMusic.playSongs(
songMids: songMids,
index: playIndex,
);
if (result['success'] == true) {
isPlaying.value = true;
totalDuration.value = song.duration;
currentPosition.value = 0;
Get.snackbar('开始播放', song.name);
} else {
Get.snackbar('播放失败', result['error'] ?? '未知错误');
}
} catch (e) {
Get.snackbar('播放失败', e.toString());
}
}
/// 暂停/继续播放
Future<void> togglePlayPause() async {
try {
if (isPlaying.value) {
final result = await _qqMusic.pauseMusic();
if (result['success'] == true) {
isPlaying.value = false;
}
} else {
final result = await _qqMusic.resumeMusic();
if (result['success'] == true) {
isPlaying.value = true;
}
}
} catch (e) {
Get.snackbar('操作失败', e.toString());
}
}
/// 上一首
Future<void> previousSong() async {
try {
final result = await _qqMusic.skipToPrevious();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value - 1;
if (newIndex < 0) newIndex = playlist.length - 1;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
}
/// 下一首
Future<void> nextSong() async {
try {
final result = await _qqMusic.skipToNext();
if (result['success'] == true) {
if (playlist.isNotEmpty) {
int newIndex = currentIndex.value + 1;
if (newIndex >= playlist.length) newIndex = 0;
currentIndex.value = newIndex;
currentSong.value = playlist[newIndex];
}
await _updateCurrentSong();
}
} catch (e) {
Get.snackbar('切换失败', e.toString());
}
}
/// 停止播放
Future<void> stopMusic() async {
try {
final result = await _qqMusic.stopMusic();
if (result['success'] == true) {
isPlaying.value = false;
currentPosition.value = 0;
}
} catch (e) {
Get.snackbar('停止失败', e.toString());
}
}
/// 更新当前播放歌曲信息
Future<void> _updateCurrentSong() async {
try {
final result = await _qqMusic.getCurrentSong();
if (result['success'] == true && result['song'] != null) {
final songData = result['song'];
final song = SongModel(
mid: songData['mid'] ?? '',
name: songData['name'] ?? '',
artist: songData['singer'] ?? '',
album: songData['album'] ?? '',
albumPic: songData['pic'],
duration: songData['duration'] ?? 0,
);
currentSong.value = song;
}
} catch (e) {
print('更新当前歌曲信息失败: $e');
}
}
/// 启动播放状态监控
void _startPlaybackStateMonitor() {
// 每秒检查一次播放状态和时间
Stream.periodic(const Duration(seconds: 1)).listen((_) async {
if (isLoggedIn.value) {
await _updatePlaybackState();
await _updatePlayTime();
}
});
}
/// 更新播放状态
Future<void> _updatePlaybackState() async {
try {
final result = await _qqMusic.getPlaybackState();
if (result['success'] == true && result['state'] != null) {
final state = result['state'];
// QQ音乐播放状态:0-停止,1-播放,2-暂停
isPlaying.value = state == 1;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 更新播放时间
Future<void> _updatePlayTime() async {
try {
final result = await _qqMusic.getPlayTime();
if (result['success'] == true) {
currentPosition.value = result['currentTime'] ?? 0;
totalDuration.value = result['totalTime'] ?? 0;
}
} catch (e) {
// 静默处理错误,避免频繁弹窗
}
}
/// 跳转到指定位置(暂时保留,QQ音乐可能不支持)
void seekTo(int position) {
currentPosition.value = position;
// QQ音乐SDK可能不支持跳转功能
Get.snackbar('提示', '当前版本不支持跳转功能');
}
@override
void onClose() {
// 清理资源
super.onClose();
}
}

49
lib/modules/qqmusic/models/song_model.dart

@ -0,0 +1,49 @@
class SongModel {
final String mid;
final String name;
final String artist;
final String album;
final String? albumPic;
final int duration;
final bool isFavorite;
SongModel({
required this.mid,
required this.name,
required this.artist,
required this.album,
this.albumPic,
required this.duration,
this.isFavorite = false,
});
factory SongModel.fromJson(Map<String, dynamic> json) {
return SongModel(
mid: json['mid'] ?? '',
name: json['name'] ?? '',
artist: json['artist'] ?? '',
album: json['album'] ?? '',
albumPic: json['albumPic'],
duration: json['duration'] ?? 0,
isFavorite: json['isFavorite'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'mid': mid,
'name': name,
'artist': artist,
'album': album,
'albumPic': albumPic,
'duration': duration,
'isFavorite': isFavorite,
};
}
String get durationText {
final minutes = duration ~/ 60;
final seconds = duration % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
}

242
lib/modules/qqmusic/views/qqmusic_view.dart

@ -0,0 +1,242 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/qqmusic_controller.dart';
import '../models/song_model.dart';
class QQMusicView extends GetView<QQMusicController> {
const QQMusicView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('QQ音乐'),
actions: [
Obx(() => controller.isLoggedIn.value
? IconButton(
icon: const Icon(Icons.logout),
onPressed: controller.logout,
)
: IconButton(
icon: const Icon(Icons.login),
onPressed: controller.login,
)),
],
),
body: Obx(() {
if (!controller.isLoggedIn.value) {
return _buildLoginView();
}
return _buildMainView();
}),
bottomNavigationBar: Obx(() {
if (controller.currentSong.value != null) {
return _buildMiniPlayer();
}
return const SizedBox.shrink();
}),
);
}
Widget _buildLoginView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.music_note,
size: 100,
color: Colors.blue,
),
const SizedBox(height: 20),
const Text(
'请先登录QQ音乐',
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: controller.login,
child: const Text('登录QQ音乐'),
),
],
),
);
}
Widget _buildMainView() {
return Column(
children: [
_buildSearchBar(),
Expanded(
child: Obx(() {
if (controller.isSearching.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.searchResults.isEmpty) {
return const Center(
child: Text('搜索歌曲开始您的音乐之旅'),
);
}
return _buildSearchResults();
}),
),
],
);
}
Widget _buildSearchBar() {
return Container(
padding: const EdgeInsets.all(16),
child: TextField(
decoration: const InputDecoration(
hintText: '搜索歌曲、歌手、专辑',
prefixIcon: Icon(Icons.search),
border: OutlineInputBorder(),
),
onSubmitted: controller.searchSongs,
),
);
}
Widget _buildSearchResults() {
return ListView.builder(
itemCount: controller.searchResults.length,
itemBuilder: (context, index) {
final song = controller.searchResults[index];
return _buildSongItem(song);
},
);
}
Widget _buildSongItem(SongModel song) {
return ListTile(
leading: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(8),
),
child: song.albumPic != null
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
song.albumPic!,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return const Icon(Icons.music_note);
},
),
)
: const Icon(Icons.music_note),
),
title: Text(
song.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
'${song.artist} • ${song.album}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(song.durationText),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => controller.playSong(
song,
songList: controller.searchResults,
),
),
],
),
);
}
Widget _buildMiniPlayer() {
return Obx(() {
final song = controller.currentSong.value!;
return Container(
height: 80,
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 4,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
// 进度条
Obx(() => LinearProgressIndicator(
value: controller.totalDuration.value > 0
? controller.currentPosition.value /
controller.totalDuration.value
: 0,
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
)),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
// 歌曲信息
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
song.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.bold),
),
Text(
song.artist,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
// 控制按钮
Row(
children: [
IconButton(
icon: const Icon(Icons.skip_previous),
onPressed: controller.previousSong,
),
Obx(() => IconButton(
icon: Icon(
controller.isPlaying.value
? Icons.pause
: Icons.play_arrow,
),
onPressed: controller.togglePlayPause,
)),
IconButton(
icon: const Icon(Icons.skip_next),
onPressed: controller.nextSong,
),
],
),
],
),
),
),
],
),
);
});
}
}

4
lib/modules/splash/controllers/splash_controller.dart

@ -104,9 +104,9 @@ class SplashController extends GetxController {
}
if (isLoggedIn) {
await getAppConfig();
Get.offAllNamed(Routes.pairing);
Get.offAllNamed(Routes.qqmusic);
} else {
Get.offAllNamed(Routes.login); // 跳转到登录页
Get.offAllNamed(Routes.qqmusic); // 跳转到登录页
}
}

8
lib/routes/app_pages.dart

@ -1,4 +1,7 @@
import 'package:get/get.dart';
import 'package:qq_music/qq_music.dart';
import 'package:voitrans/modules/qqmusic/bindings/qqmusic_binding.dart';
import 'package:voitrans/modules/qqmusic/views/qqmusic_view.dart';
import '/modules/ble/bindings/ble_binding.dart';
import '../modules/ble/views/ble_view.dart';
@ -275,5 +278,10 @@ abstract class AppPages {
page: () => const BleView(),
binding: BleBinding(),
),
GetPage(
name: Routes.qqmusic,
page: () => const QQMusicView(),
binding: QQMusicBinding(),
),
];
}

2
lib/routes/app_routes.dart

@ -61,4 +61,6 @@ abstract class Routes {
// ble设备管理
static const ble = '/ble';
// ble设备管理
static const qqmusic = '/qqmusic';
}

33
local_plugins/qq_music/.gitignore

@ -0,0 +1,33 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
build/

33
local_plugins/qq_music/.metadata

@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "5c1433509f997e9059d230942bdf83f1b88a179e"
channel: "stable"
project_type: plugin
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
- platform: android
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
- platform: ios
create_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
base_revision: 5c1433509f997e9059d230942bdf83f1b88a179e
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

3
local_plugins/qq_music/CHANGELOG.md

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

1
local_plugins/qq_music/LICENSE

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

15
local_plugins/qq_music/README.md

@ -0,0 +1,15 @@
# qq_music
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter
[plug-in package](https://flutter.dev/to/develop-plugins),
a specialized package that includes platform-specific implementation code for
Android and/or iOS.
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

4
local_plugins/qq_music/analysis_options.yaml

@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

9
local_plugins/qq_music/android/.gitignore

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

79
local_plugins/qq_music/android/build.gradle

@ -0,0 +1,79 @@
group = "com.yunqiinnovation.qq_music"
version = "1.0-SNAPSHOT"
buildscript {
ext.kotlin_version = "2.1.0"
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:8.7.3")
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")
}
}
allprojects {
repositories {
google()
mavenCentral()
// 添加本地AAR仓库
flatDir {
dirs 'libs'
}
}
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
android {
namespace = "com.yunqiinnovation.qq_music"
compileSdk = 35
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11
}
// 合并后的 sourceSets 配置
sourceSets {
main {
java.srcDirs += "src/main/kotlin"
aidl.srcDirs = ['src/main/aidl']
}
test {
java.srcDirs += "src/test/kotlin"
}
}
defaultConfig {
minSdk = 21
}
buildFeatures {
aidl true
}
dependencies {
testImplementation("org.jetbrains.kotlin:kotlin-test")
testImplementation("org.mockito:mockito-core:5.0.0")
// 添加AndroidX AppCompat依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
// 添加 Gson 依赖
implementation 'com.google.code.gson:gson:2.10.1'
// 添加 ConstraintLayout 依赖
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
api(name: 'qqmusic-innovation-aidl-api-sdk-1.0.26', ext: 'aar')
}
}

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

Binary file not shown.

1
local_plugins/qq_music/android/settings.gradle

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

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

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

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

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

361
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<String>, 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<Data.Song>, index: Int, block: (() -> Unit)) {
val midList = ArrayList<String>()
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<String>, 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<String>, 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<String>, 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<String>, 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<String>, 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<String>, 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<String>, 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<String>, block: (() -> Unit)) {
api?.registerEventListener(events, object : IQQMusicApiEventListener.Stub() {
override fun onEvent(event: String, extra: Bundle) {
block()
}
})
}
fun unregisterEventListener(api: IQQMusicApi?, events: ArrayList<String>, 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<String>, 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<String>, 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<String>, 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<String>, 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)
}
})
}
}

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

239
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicLoginManager.kt

@ -0,0 +1,239 @@
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 = "qqmusicapidemo://"
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("BIND_FAILED", "绑定QQ音乐服务失败,请确保已安装QQ音乐")
}
}
}
/**
* 检查授权状态
*/
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, "已授权,直接获取登录信息")
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)
}
}
}

495
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QQMusicManager.kt

@ -0,0 +1,495 @@
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.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<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)
}
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消息
sayHi()
}
override fun onServiceDisconnected(name: ComponentName?) {
Log.d(TAG, "QQ音乐服务连接断开")
qqmusicApi = null
isBindQQMusicService = false
}
}
init {
bindQQMusicApiService()
}
private fun sayHi() {
if (!isBindQQMusicService || qqmusicApi == null) return
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))
}
private 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<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 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 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
}
} catch (e: Exception) {
Log.e(TAG, "清理资源异常", e)
}
qqmusicApi = null
}
}

393
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/QqMusicPlugin.kt

@ -0,0 +1,393 @@
package com.yunqiinnovation.qq_music
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.app.Activity
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 var loginManager: QQMusicLoginManager? = null
private var musicManager: QQMusicManager? = null
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "com.yunqiinnovation.qq_music")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"login" -> {
handleLogin(result)
}
"searchSongs" -> {
handleSearchSongs(call, result)
}
"playSongs" -> {
handlePlaySongs(call, result)
}
"playMusic" -> {
handlePlayMusic(result)
}
"pauseMusic" -> {
handlePauseMusic(result)
}
"resumeMusic" -> {
handleResumeMusic(result)
}
"stopMusic" -> {
handleStopMusic(result)
}
"skipToNext" -> {
handleSkipToNext(result)
}
"skipToPrevious" -> {
handleSkipToPrevious(result)
}
"getPlaybackState" -> {
handleGetPlaybackState(result)
}
"getCurrentSong" -> {
handleGetCurrentSong(result)
}
"getPlayTime" -> {
handleGetPlayTime(result)
}
"getPlatformVersion" -> {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
}
else -> {
result.notImplemented()
}
}
}
private fun handleLogin(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
// 创建登录管理器
loginManager = QQMusicLoginManager(currentActivity)
// 开始登录
loginManager?.startLogin(object : QQMusicLoginManager.LoginCallback {
override fun onSuccess(openId: String, openToken: String, expireTime: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
val loginResult = mapOf(
"success" to true,
"openId" to openId,
"openToken" to openToken,
"expireTime" to expireTime
)
Log.d("QqMusicPlugin", "登录成功回调: $loginResult")
result.success(loginResult)
}
}
override fun onError(errorCode: String, errorMessage: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.e("QqMusicPlugin", "登录失败回调: $errorCode - $errorMessage")
result.error(errorCode, errorMessage, null)
}
}
})
}
private fun handleSearchSongs(call: MethodCall, result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
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
}
Log.d("QqMusicPlugin", "开始搜索: keyword=$keyword, searchType=$searchType")
ensureMusicManager(currentActivity)
musicManager?.searchSongs(keyword, searchType, object : QQMusicManager.SearchCallback {
override fun onSuccess(songs: List<Map<String, Any?>>) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.d("QqMusicPlugin", "搜索成功: 找到 ${songs.size} 首歌曲")
result.success(mapOf(
"success" to true,
"songs" to songs
))
}
}
override fun onError(errorCode: Int, errorMessage: String) {
// 确保在主线程中执行回调
Handler(Looper.getMainLooper()).post {
Log.e("QqMusicPlugin", "搜索失败: code=$errorCode, message=$errorMessage")
result.error("SEARCH_FAILED", errorMessage, null)
}
}
})
}
private fun handlePlaySongs(call: MethodCall, result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
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
}
ensureMusicManager(currentActivity)
musicManager?.playSongs(songMids, index, object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PLAY_FAILED", errorMessage, null)
}
})
}
private fun handlePlayMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.playMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PLAY_FAILED", errorMessage, null)
}
})
}
private fun handlePauseMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.pauseMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("PAUSE_FAILED", errorMessage, null)
}
})
}
private fun handleResumeMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.resumeMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("RESUME_FAILED", errorMessage, null)
}
})
}
private fun handleStopMusic(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.stopMusic(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("STOP_FAILED", errorMessage, null)
}
})
}
private fun handleSkipToNext(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.skipToNext(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("SKIP_FAILED", errorMessage, null)
}
})
}
private fun handleSkipToPrevious(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.skipToPrevious(object : QQMusicManager.PlayCallback {
override fun onSuccess() {
result.success(mapOf("success" to true))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("SKIP_FAILED", errorMessage, null)
}
})
}
private fun handleGetPlaybackState(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.getPlaybackState(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
result.success(mapOf(
"success" to true,
"state" to data["state"]
))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_STATE_FAILED", errorMessage, null)
}
})
}
private fun handleGetCurrentSong(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.getCurrentSong(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
result.success(mapOf(
"success" to true,
"song" to data
))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_SONG_FAILED", errorMessage, null)
}
})
}
private fun handleGetPlayTime(result: Result) {
val currentActivity = activity
if (currentActivity == null) {
result.error("NO_ACTIVITY", "Activity not available", null)
return
}
ensureMusicManager(currentActivity)
musicManager?.getPlayTime(object : QQMusicManager.InfoCallback {
override fun onSuccess(data: Map<String, Any?>) {
result.success(mapOf(
"success" to true,
"totalTime" to data["totalTime"],
"currentTime" to data["currentTime"]
))
}
override fun onError(errorCode: Int, errorMessage: String) {
result.error("GET_TIME_FAILED", errorMessage, null)
}
})
}
private fun ensureMusicManager(activity: Activity) {
if (musicManager == null) {
musicManager = QQMusicManager(activity)
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
loginManager?.cleanup()
loginManager = null
musicManager?.cleanup()
musicManager = null
}
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivityForConfigChanges() {
activity = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity
}
override fun onDetachedFromActivity() {
activity = null
loginManager?.cleanup()
loginManager = null
musicManager?.cleanup()
musicManager = null
}
}

64
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, "qqmusicapidemo://");
String sourceString = signJson.toString();
//2. 使用Q音公钥加密(随机数+签名)
byte[] encryptData = RSAUtils.encryptByPublicKey(sourceString.getBytes(), QQMusicPublicKey);
if (encryptData == null) {
return null;
}
return Base64.encodeToString(encryptData, Base64.DEFAULT);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static String decryptQQMEncryptString(String qmEncryptString) {
try {
byte[] qmEncryptData = Base64.decode(qmEncryptString, Base64.DEFAULT);
//7.使用App私钥解密 - 修改这里,使用Base64格式的私钥
byte[] decryptData = RSAUtils.decryptByPrivateKey(qmEncryptData, Config.getBase64PrivateKey());
if (decryptData != null) {
return new String(decryptData);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static boolean checkQMSign(String sign, String nonce) {
try {
return RSAUtils.verify(nonce.getBytes(), QQMusicPublicKey, sign);
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
}

314
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<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(1024);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<>(2);
keyMap.put(PUBLIC_KEY, publicKey);
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
* 用私钥对信息生成数字签名
*
* @param data 已加密数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static String sign(byte[] data, String privateKey) throws Exception {
byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initSign(privateK);
signature.update(data);
return Base64.encodeToString(signature.sign(), Base64.DEFAULT);
}
/**
* 校验数字签名
*
* @param data 已加密数据
* @param publicKey 公钥(BASE64编码)
* @param sign 数字签名
* @return
* @throws Exception
*/
public static boolean verify(byte[] data, String publicKey, String sign)
throws Exception {
byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
PublicKey publicK = keyFactory.generatePublic(keySpec);
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
signature.initVerify(publicK);
signature.update(data);
return signature.verify(Base64.decode(sign, Base64.DEFAULT));
}
/**
* 私钥解密
*
* @param encryptedData 已加密数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey)
throws Exception {
byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance(CIHPER_KEY);
cipher.init(Cipher.DECRYPT_MODE, privateK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
public static String testDecryptByJavaPrivKey(String privkey_from_java, String encryptedString) throws Exception {
//加解密类
Cipher cipher = Cipher.getInstance("RSA");//Cipher.getInstance("RSA/ECB/PKCS1Padding");
// G0语言通过java产生的公钥加密得到的加密后的字符串
byte[] enBytes = Base64.decode(encryptedString, Base64.DEFAULT);
//通过密钥字符串得到密钥
PrivateKey privateKey = testGetPrivateKey(privkey_from_java);
//解密
cipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] deBytes = cipher.doFinal(enBytes);
return new String(deBytes);
}
public static PrivateKey testGetPrivateKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = Base64.decode(key, Base64.DEFAULT);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(keySpec);
}
/**
* 公钥解密
*
* @param encryptedData 已加密数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] decryptByPublicKey(byte[] encryptedData, String publicKey)
throws Exception {
byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance(CIHPER_KEY);
cipher.init(Cipher.DECRYPT_MODE, publicK);
int inputLen = encryptedData.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段解密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_DECRYPT_BLOCK) {
cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK);
} else {
cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_DECRYPT_BLOCK;
}
byte[] decryptedData = out.toByteArray();
out.close();
return decryptedData;
}
/**
* 公钥加密
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey)
throws Exception {
byte[] keyBytes = Base64.decode(publicKey, Base64.DEFAULT);
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key publicK = keyFactory.generatePublic(x509KeySpec);
// 对数据加密
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance(CIHPER_KEY);
cipher.init(Cipher.ENCRYPT_MODE, publicK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/**
* 私钥加密
*
* @param data 源数据
* @param privateKey 私钥(BASE64编码)
* @return
* @throws Exception
*/
public static byte[] encryptByPrivateKey(byte[] data, String privateKey)
throws Exception {
byte[] keyBytes = Base64.decode(privateKey, Base64.DEFAULT);
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM);
Key privateK = keyFactory.generatePrivate(pkcs8KeySpec);
// Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
Cipher cipher = Cipher.getInstance(CIHPER_KEY);
cipher.init(Cipher.ENCRYPT_MODE, privateK);
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > MAX_ENCRYPT_BLOCK) {
cache = cipher.doFinal(data, offSet, MAX_ENCRYPT_BLOCK);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * MAX_ENCRYPT_BLOCK;
}
byte[] encryptedData = out.toByteArray();
out.close();
return encryptedData;
}
/**
* 获取私钥
*
* @param keyMap 密钥对
* @return
*/
public static String getPrivateKey(Map<String, Object> keyMap) {
Key key = (Key) keyMap.get(PRIVATE_KEY);
return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
}
/**
* 获取公钥
*
* @param keyMap 密钥对
* @return
*/
public static String getPublicKey(Map<String, Object> keyMap) {
Key key = (Key) keyMap.get(PUBLIC_KEY);
return Base64.encodeToString(key.getEncoded(), Base64.DEFAULT);
}
}

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

79
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<Button>(R.id.bt_bind_service).setOnClickListener {
playService?.bindService()
}
findViewById<Button>(R.id.bt_start_pcm_mode).setOnClickListener {
playService?.startPcmMode()
Toast.makeText(this, "进入PCM模式", Toast.LENGTH_SHORT).show()
}
findViewById<Button>(R.id.bt_stop_pcm_mode).setOnClickListener {
playService?.stopPcmMode()
Toast.makeText(this, "退出PCM模式", Toast.LENGTH_SHORT).show()
}
findViewById<Button>(R.id.bt_play).setOnClickListener {
playService?.resumeOrPause()
}
findViewById<Button>(R.id.bt_next).setOnClickListener {
playService?.playNext()
}
}
override fun onDestroy() {
super.onDestroy()
unbindService(conn)
}
}

383
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/pcm/PlayerService.kt

@ -0,0 +1,383 @@
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.*
/**
* 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 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 notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_launcher_background)
.setContentTitle("demo标题")
.setContentText("demo正在播放音乐")
.build()
startForeground(1, notification)
}
return super.onStartCommand(intent, flags, startId)
}
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,
"qqmusicapidemo://"
)
} else {
Toast.makeText(this@PlayerService, "授权成功", Toast.LENGTH_SHORT).show()
qqMusicApi?.registerEventListener(
arrayListOf(
Events.API_EVENT_PLAY_STATE_CHANGED,
Events.API_EVENT_PLAY_SONG_CHANGED,
Events.API_EVENT_MEDIA_INFO_CHANGED
), eventListener
)
}
}
}
}
private fun isPlaying(): Boolean {
Log.d(TAG, "[isPlaying]curPlayState:$curPlayState")
return (curPlayState == PlayState.STARTED
|| curPlayState == PlayState.INITIALIZED
|| curPlayState == PlayState.PREPARED
|| curPlayState == PlayState.PREPARING
|| curPlayState == PlayState.BUFFERING)
}
}
/**
* Bundle 扩展函数,用于将 Bundle 转换为可打印的字符串
*/
fun Bundle.toPrintableString(): String {
val sb = StringBuilder()
sb.append("{")
for (key in keySet()) {
if (sb.length > 1) {
sb.append(", ")
}
val value = get(key)
sb.append(key).append("=")
when (value) {
is String -> sb.append("\"$value\"")
is Bundle -> sb.append(value.toPrintableString())
is Array<*> -> sb.append(value.contentToString())
is IntArray -> sb.append(value.contentToString())
is LongArray -> sb.append(value.contentToString())
is FloatArray -> sb.append(value.contentToString())
is DoubleArray -> sb.append(value.contentToString())
is BooleanArray -> sb.append(value.contentToString())
is ByteArray -> sb.append(value.contentToString())
is CharArray -> sb.append(value.contentToString())
is ShortArray -> sb.append(value.contentToString())
else -> sb.append(value?.toString() ?: "null")
}
}
sb.append("}")
return sb.toString()
}

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

@ -0,0 +1,17 @@
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;
}
}

177
local_plugins/qq_music/android/src/main/kotlin/com/yunqiinnovation/qq_music/util/QPlayBindHelper.kt

@ -0,0 +1,177 @@
package com.yunqiinnovation.qq_music.util
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
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
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)
}
fun ensureQQMusicBindByStartProcess(isStartProcess: Boolean = true, callback: ((Boolean) -> Unit)? = null) {
bindQQMusic({ succeed ->
if (!succeed) {
if (isStartProcess) {
CommonCmd.startQQMusicProcess(context, context.packageName)
}
handler.postDelayed({
ensureQQMusicBindByStartProcess(false, callback)
}, 800)
} else {
callback?.invoke(true)
}
}, true)
}
fun isBindQQMusic() = api != null
fun getQQMusicApi() = api
private fun getQQMusicApiServiceIntent(): Intent {
var intent = Intent("com.tencent.qqmusic.third.api.QQMusicApiService")
// 必须显式绑定
when (bindPlatform) {
CommonCmd.AIDL_PLATFORM_TYPE_PHONE -> {
intent = Intent("com.tencent.qqmusic.third.api.QQMusicApiService")
intent.`package` = "com.tencent.qqmusic"
}
CommonCmd.AIDL_PLATFORM_TYPE_CAR -> {
intent = Intent("com.tencent.qqmusiccar.third.api.QQMusicApiService")
intent.`package` = "com.tencent.qqmusiccar"
}
CommonCmd.AIDL_PLATFORM_TYPE_TV -> {
intent = Intent("com.tencent.qqmusictv.third.api.QQMusicApiService")
intent.`package` = "com.tencent.qqmusictv"
}
else -> {
Log.e(TAG, "platform 不匹配")
Toast.makeText(context, "请先在Config中填写配置信息!", Toast.LENGTH_SHORT).show()
}
}
return intent
}
/**
* 重新绑定qq音乐
*/
private fun reBindQQMusicService() {
CommonCmd.startQQMusicProcess(context, context.packageName)
handler.postDelayed(Runnable {
ensureQQMusicBind {
if (it) {
Log.e(TAG, "重新绑定成功")
reBinding = false
} else {
Log.e(TAG, "重新绑定失败")
reBindQQMusicService()
}
}
}, 200)
reBinding = true
}
}
fun Context.unbindServiceSafe(conn: ServiceConnection) {
try {
unbindService(conn)
} catch (e: Throwable) {
Log.e("CommExt", "unbindService failed", e)
}
}

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

@ -0,0 +1,105 @@
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}]")
}
}
}
}

34
local_plugins/qq_music/android/src/main/res/drawable-v24/ic_launcher_foreground.xml

@ -0,0 +1,34 @@
<vector xmlns:aapt="http://schemas.android.com/aapt"
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportHeight="108"
android:viewportWidth="108">
<path
android:fillType="evenOdd"
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
android:strokeColor="#00000000"
android:strokeWidth="1">
<aapt:attr name="android:fillColor">
<gradient
android:endX="78.5885"
android:endY="90.9159"
android:startX="48.7653"
android:startY="61.0927"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0"/>
<item
android:color="#00000000"
android:offset="1.0"/>
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
android:strokeColor="#00000000"
android:strokeWidth="1"/>
</vector>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

BIN
local_plugins/qq_music/android/src/main/res/mipmap-hdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-hdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-mdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-mdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
local_plugins/qq_music/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

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

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

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

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

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

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

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

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

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

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

38
local_plugins/qq_music/ios/.gitignore

@ -0,0 +1,38 @@
.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

29
local_plugins/qq_music/ios/qq_music.podspec

@ -0,0 +1,29 @@
#
# 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.dependency 'Flutter'
s.platform = :ios, '12.0'
# 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'
# If your plugin requires a privacy manifest, for example if it uses any
# required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
# plugin's privacy impact, and then uncomment this line. For more information,
# see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
# s.resource_bundles = {'qq_music_privacy' => ['qq_music/Sources/qq_music/PrivacyInfo.xcprivacy']}
end

32
local_plugins/qq_music/ios/qq_music/Package.swift

@ -0,0 +1,32 @@
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "qq_music",
platforms: [
.iOS("12.0")
],
products: [
.library(name: "qq-music", targets: ["qq_music"])
],
dependencies: [],
targets: [
.target(
name: "qq_music",
dependencies: [],
resources: [
// If your plugin requires a privacy manifest, for example if it uses any required
// reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's
// privacy impact, and then uncomment these lines. For more information, see
// https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
// .process("PrivacyInfo.xcprivacy"),
// If you have other resources that need to be bundled with your plugin, refer to
// the following instructions to add them:
// https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package
]
)
]
)

14
local_plugins/qq_music/ios/qq_music/Sources/qq_music/PrivacyInfo.xcprivacy

@ -0,0 +1,14 @@
<?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>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTracking</key>
<false/>
</dict>
</plist>

19
local_plugins/qq_music/ios/qq_music/Sources/qq_music/QqMusicPlugin.swift

@ -0,0 +1,19 @@
import Flutter
import UIKit
public class QqMusicPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "qq_music", binaryMessenger: registrar.messenger())
let instance = QqMusicPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "getPlatformVersion":
result("iOS " + UIDevice.current.systemVersion)
default:
result(FlutterMethodNotImplemented)
}
}
}

279
local_plugins/qq_music/lib/qq_music.dart

@ -0,0 +1,279 @@
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>> 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,
};
}
}
}
/// 登录结果类
class QQMusicLoginResult {
final bool success;
final String? openId;
final String? openToken;
final String? expireTime;
final String? error;
final String? code;
QQMusicLoginResult({
required this.success,
this.openId,
this.openToken,
this.expireTime,
this.error,
this.code,
});
factory QQMusicLoginResult.fromMap(Map<String, dynamic> map) {
return QQMusicLoginResult(
success: map['success'] ?? false,
openId: map['openId'],
openToken: map['openToken'],
expireTime: map['expireTime'],
error: map['error'],
code: map['code'],
);
}
}
/// 搜索结果类
class QQMusicSearchResult {
final bool success;
final List<QQMusicSong>? songs;
final String? error;
final String? code;
QQMusicSearchResult({
required this.success,
this.songs,
this.error,
this.code,
});
factory QQMusicSearchResult.fromMap(Map<String, dynamic> map) {
List<QQMusicSong>? songs;
if (map['songs'] != null) {
songs = (map['songs'] as List)
.map((song) => QQMusicSong.fromMap(Map<String, dynamic>.from(song)))
.toList();
}
return QQMusicSearchResult(
success: map['success'] ?? false,
songs: songs,
error: map['error'],
code: map['code'],
);
}
}
/// 歌曲信息类
class QQMusicSong {
final String? mid;
final String? id;
final String? name;
final String? singer;
final String? album;
final int? duration;
final String? pic;
QQMusicSong({
this.mid,
this.id,
this.name,
this.singer,
this.album,
this.duration,
this.pic,
});
factory QQMusicSong.fromMap(Map<String, dynamic> map) {
return QQMusicSong(
mid: map['mid'],
id: map['id'],
name: map['name'],
singer: map['singer'],
album: map['album'],
duration: map['duration'],
pic: map['pic'],
);
}
Map<String, dynamic> toMap() {
return {
'mid': mid,
'id': id,
'name': name,
'singer': singer,
'album': album,
'duration': duration,
'pic': pic,
};
}
}

28
local_plugins/qq_music/pubspec.yaml

@ -0,0 +1,28 @@
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

2
pubspec.yaml

@ -104,6 +104,8 @@ dependencies:
path: local_plugins/jl_opus
user_portrait:
path: local_plugins/user_portrait
qq_music:
path: local_plugins/qq_music
test:
path: local_plugins/test

Loading…
Cancel
Save