@ -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()}'}; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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()); |
||||
|
} |
||||
|
} |
||||
@ -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(); |
||||
|
} |
||||
|
} |
||||
@ -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')}'; |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
), |
||||
|
), |
||||
|
], |
||||
|
), |
||||
|
); |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
@ -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/ |
||||
@ -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' |
||||
@ -0,0 +1,3 @@ |
|||||
|
## 0.0.1 |
||||
|
|
||||
|
* TODO: Describe initial release. |
||||
@ -0,0 +1 @@ |
|||||
|
TODO: Add your license here. |
||||
@ -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. |
||||
|
|
||||
@ -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 |
||||
@ -0,0 +1,9 @@ |
|||||
|
*.iml |
||||
|
.gradle |
||||
|
/local.properties |
||||
|
/.idea/workspace.xml |
||||
|
/.idea/libraries |
||||
|
.DS_Store |
||||
|
/build |
||||
|
/captures |
||||
|
.cxx |
||||
@ -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') |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1 @@ |
|||||
|
rootProject.name = 'qq_music' |
||||
@ -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> |
||||
@ -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); |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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; |
||||
|
|
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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); |
||||
|
} |
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
@ -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() |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
@ -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}]") |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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> |
||||
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 15 KiB |
@ -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> |
||||
@ -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> |
||||
@ -0,0 +1,4 @@ |
|||||
|
<resources> |
||||
|
<dimen name="fab_margin">16dp</dimen> |
||||
|
<dimen name="dimen_width">80dp</dimen> |
||||
|
</resources> |
||||
@ -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> |
||||
@ -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> |
||||
@ -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 |
||||
@ -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 |
||||
@ -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 |
||||
|
] |
||||
|
) |
||||
|
] |
||||
|
) |
||||
@ -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> |
||||
@ -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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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, |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -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 |
||||