Browse Source

Merge commit 'b7dfc99c6d7fd3c12668ddfec7534e5c827f0124' into lxm

weicu
lxm 1 year ago
parent
commit
7b22fb7e45
  1. 4
      lib/data/services/music_manager.dart
  2. 68
      lib/modules/agent/controllers/agent_controller.dart
  3. 29
      lib/modules/agent/views/message_bubble.dart
  4. 63
      lib/modules/pairing/controllers/pairing_controller.dart
  5. 109
      local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt
  6. 100
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt
  7. 30
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt
  8. 36
      local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt
  9. 4
      local_plugins/music_service/lib/music_service.dart

4
lib/data/services/music_manager.dart

@ -23,8 +23,8 @@ class MusicManager extends GetxService {
}
//播放
Future<void> play(String url) async {
await _musicService.playMusic(url);
Future<void> play(Map<String, String> song) async {
await _musicService.playMusic(song);
}
//暂停

68
lib/modules/agent/controllers/agent_controller.dart

@ -222,7 +222,8 @@ class AgentController extends GetxController {
messages.refresh();
} else {
// 如果没有找到识别中的消息,添加新消息
messages.add(Message(isUser: true, text: text, isRecognizing: false));
messages
.add(Message(isUser: true, text: text, isRecognizing: false));
}
isProcessing.value = true;
@ -346,35 +347,38 @@ class AgentController extends GetxController {
break;
case AgentServiceEventType.functionCallResult:
final responseId = event.data['responseId'] ?? '';
final result = event.data['result'] ?? '';
final parsedResult = result.isNotEmpty ? jsonDecode(result) : {};
final meta = parsedResult["meta"] ?? {};
// final functionCall = event.data['function_call'] ?? '';
logger.d('mcp 执行结果meta: $meta');
// if (meta.isNotEmpty) {
// // logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// // 判断是否为新的回复或响应ID是否改变
// if (_isNewAssistantResponse ) {
// // 添加新消息
// messages.add(Message(isUser: false, text: "", meta: meta));
// _isNewAssistantResponse = false;
// } else {
// // 更新最后一条消息
// final lastIndex = messages.length - 1;
// if (lastIndex >= 0 && !messages[lastIndex].isUser) {
// messages[lastIndex] = Message(
// isUser: false,
// text: messages[lastIndex].text,
// meta: meta,
// timestamp: messages[lastIndex].timestamp,
// );
// }
// }
// _scrollToBottom();
// }
logger.i('mcp 结果处理完成');
final functionCall = event.data['function_call'] ?? '';
logger.i('Flutter 结束调用 mcp: $result, $functionCall');
if (result.isNotEmpty) {
final parsedResult = jsonDecode(result);
final meta = parsedResult["meta"] ?? "";
final metaResult = jsonDecode(meta);
logger.i('Flutter 结束调用 mcp metaResult: $metaResult');
// 判断是否为新的回复或响应ID是否改变
if (_isNewAssistantResponse || _currentResponseId != responseId) {
// 添加新消息
messages.add(Message(isUser: false, text: "", meta: metaResult));
_isNewAssistantResponse = false;
_currentResponseId = responseId;
} else {
// 更新最后一条消息
final lastIndex = messages.length - 1;
if (lastIndex >= 0 && !messages[lastIndex].isUser) {
messages[lastIndex] = Message(
isUser: false,
text: messages[lastIndex].text,
meta: meta,
timestamp: messages[lastIndex].timestamp,
);
}
}
// 滚动到底部
_scrollToBottom();
}
break;
default:
break;
@ -625,10 +629,10 @@ class AgentController extends GetxController {
}
// 播放音乐
void musicPlay(String url) {
_musiceManager.play(url);
void musicPlay(Map<String, String> song) {
_musiceManager.play(song);
isPlaying.value = true;
currentUrl.value = url;
currentUrl.value = song['url'] ?? '';
}
// 播放音乐

29
lib/modules/agent/views/message_bubble.dart

@ -197,23 +197,22 @@ class MessageBubble extends GetView<AgentController> {
// 判断消息内容,显示特殊卡片
if (!isUser && mate != null) {
if (mate!.keys.contains('card_music')) {
final musicId = mate!['card_music']['id'] as String;
final musicName = mate!['card_music']['name'] as String;
final musicUrl = mate!['card_music']['url'] as String;
final musicSinger = mate!['card_music']['sgener'] as String;
final musicImage = mate!['card_music']['image'] as String;
//音乐卡片
return _buildMusicItem(context, musicName, musicUrl, musicSinger);
return _buildMusicItem(
context, musicId, musicName, musicUrl, musicSinger, musicImage);
} else if (mate!.keys.contains('card_musiclist')) {
final rawList = mate!['card_musiclist']['musics'] as List;
final playlist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
// final playlist =
// mate!['card_musiclist']['musics'] as List<Map<String, String>>;
//音乐播放列表
return _buildMusicContent(context, playlist);
} else if (mate!.keys.contains('card_weather')) {
final city = mate!['card_weather']['city'] as String;
final rawList = mate!['card_weather']['dayweather'] as List;
final daylist =
rawList.map((e) => Map<String, dynamic>.from(e)).toList();
@ -327,13 +326,14 @@ class MessageBubble extends GetView<AgentController> {
...playlist.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
final id = item["id"] ?? "";
final name = item["name"] ?? "未知歌曲";
final url = item["url"] ?? "";
final singer = item["sgener"] ?? "未知歌手";
final image = item["image"] ?? "";
return Column(
children: [
_buildMusicItem(context, name, url, singer),
_buildMusicItem(context, id, name, url, singer, image),
if (index != playlist.length - 1)
Divider(height: 1, thickness: 1, color: Colors.grey[100]),
],
@ -531,9 +531,16 @@ class MessageBubble extends GetView<AgentController> {
}
/// 构建音乐项目
Widget _buildMusicItem(
BuildContext context, String name, String url, String singer) {
Widget _buildMusicItem(BuildContext context, String id, String name,
String url, String singer, String image) {
final controller = Get.find<AgentController>(); // 获取控制器
Map<String, String> song = {
'id': id,
'url': url,
'title': name,
'artist': singer,
'coverUrl': image,
};
controller.musicStatus(); //刷新状态
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
@ -588,13 +595,13 @@ class MessageBubble extends GetView<AgentController> {
if (controller.currentUrl.value == url) {
controller.musicPause();
} else {
controller.musicPlay(url);
controller.musicPlay(song);
}
} else {
if (controller.currentUrl.value == url) {
controller.musicResume();
} else {
controller.musicPlay(url);
controller.musicPlay(song);
}
}
},

63
lib/modules/pairing/controllers/pairing_controller.dart

@ -14,6 +14,8 @@ import 'package:ble_service/ble_service.dart';
class PairingController extends GetxController {
static PairingController get to => Get.find();
// 需要设备
final bool needdeivice = false;
// 状态标志
final isLoading = false.obs;
@ -28,7 +30,7 @@ class PairingController extends GetxController {
// 状态描述(用于UI显示)
final statusDescription = 'initializing'.tr.obs;
// 按钮文本
final actionButtonText = '去系统设置'.obs;
@ -117,7 +119,7 @@ class PairingController extends GetxController {
// 刷新连接设备并检查配对状态
statusDescription.value = 'searchingDevice'.tr;
await _bluetoothService.refreshDeviceConnection();
// 设备连接时检查配对状态
if (isDeviceConnected) {
await _checkDevicePairingStatus();
@ -146,7 +148,7 @@ class PairingController extends GetxController {
}
Logger.info('发现已关联的Companion设备: $pairedMac');
// 检查当前连接设备与已保存的设备是否一致
if (currentDevice!.address != pairedMac) {
// MAC地址不匹配
@ -154,13 +156,13 @@ class PairingController extends GetxController {
statusDescription.value = 'pleaseConnectPairedDevice'.tr;
return;
}
// 判断设备名称是否符合要求
if (!_isValidDeviceName(currentDevice!.name)) {
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
return;
}
// 设备匹配且合规,标记为已配对
Logger.info('当前设备与已配对设备匹配');
_companionAssociated.value = true;
@ -223,7 +225,7 @@ class PairingController extends GetxController {
_bluetoothState.value = state;
_updateStatusDescription();
});
// 监听设备连接状态变化
_deviceWorker =
ever(_bluetoothService.currentDeviceRx, (BluetoothDevice? device) {
@ -238,12 +240,13 @@ class PairingController extends GetxController {
/// 处理设备连接成功事件
Future<void> _handleDeviceConnected() async {
if (!isDeviceConnected || _isNavigatingToHome) return;
// 检查配对状态,决定下一步操作
await _checkDevicePairingStatus();
// 如果还未配对且设备合规,启动配对流程
if (!_companionAssociated.value && _isValidDeviceName(currentDevice!.name)) {
if (!_companionAssociated.value &&
_isValidDeviceName(currentDevice!.name)) {
_startCompanionAssociation();
}
}
@ -256,7 +259,7 @@ class PairingController extends GetxController {
actionButtonText.value = '关联DeapSound耳机';
return;
}
if (!hasPermissions) {
statusDescription.value = 'permissionDenied'.tr;
actionButtonText.value = '去系统设置';
@ -271,9 +274,11 @@ class PairingController extends GetxController {
if (isDeviceConnected) {
if (Platform.isAndroid && !isCompanionAssociated && _isAssociating) {
statusDescription.value = '${'associatingDevice'.tr} ${currentDevice!.name}';
statusDescription.value =
'${'associatingDevice'.tr} ${currentDevice!.name}';
} else {
statusDescription.value = '${'connectedDevice'.tr} ${currentDevice!.name}';
statusDescription.value =
'${'connectedDevice'.tr} ${currentDevice!.name}';
}
return;
}
@ -286,21 +291,20 @@ class PairingController extends GetxController {
statusDescription.value = 'notFound'.tr;
}
/// 启动Companion设备关联流程
Future<void> _startCompanionAssociation() async {
// 如果不是Android平台,已跳转,已完成关联或正在进行关联,直接返回
if (!Platform.isAndroid ||
_isNavigatingToHome ||
_companionAssociated.value ||
_isAssociating ||
if (!Platform.isAndroid ||
_isNavigatingToHome ||
_companionAssociated.value ||
_isAssociating ||
!isDeviceConnected) {
return;
}
// 重置拒绝状态
_userRejectedAssociation.value = false;
// 设置关联状态标志
_isAssociating = true;
_updateStatusDescription();
@ -314,14 +318,14 @@ class PairingController extends GetxController {
_autoNavigateToHome();
return;
}
// 检查设备名称是否符合要求
if (!_isValidDeviceName(currentDevice!.name)) {
statusDescription.value = 'pleaseConnectDeepSoundDevice'.tr;
_isAssociating = false;
return;
}
Logger.info('开始Companion设备关联流程, 设备MAC: ${currentDevice!.address}');
var success = await _bleService.startDeviceAssociation(
@ -329,7 +333,7 @@ class PairingController extends GetxController {
macAddress: currentDevice!.address,
);
_isAssociating = false;
if (success) {
Logger.info('Companion设备关联成功');
_companionAssociated.value = true;
@ -338,8 +342,7 @@ class PairingController extends GetxController {
// 注册后台扫描,确保应用被杀死后仍能被唤醒
await _bleService.registerBackgroundScan(
macAddress: currentDevice!.address);
// 关联成功后跳转
_autoNavigateToHome();
} else {
@ -359,9 +362,9 @@ class PairingController extends GetxController {
/// 自动跳转到主页
Future<void> _autoNavigateToHome() async {
if (_isNavigatingToHome || !isDeviceConnected) return;
if (needdeivice && (_isNavigatingToHome || !isDeviceConnected)) return;
// 启动BLE后台服务
// 启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
@ -375,7 +378,7 @@ class PairingController extends GetxController {
/// 启动BLE后台服务
Future<void> _startAgentService() async {
try {
if (!isDeviceConnected) return;
if (needdeivice && !isDeviceConnected) return;
final result = await BleManager.to.startAgentService();
Logger.info(result ? 'BLE后台服务启动成功' : 'BLE后台服务启动失败');
@ -426,7 +429,7 @@ class PairingController extends GetxController {
Future<void> openAppSettings() async {
await AppSettings.openAppSettings();
}
/// 重新开始关联
void restartAssociation() {
if (isDeviceConnected && !isCompanionAssociated) {
@ -435,8 +438,8 @@ class PairingController extends GetxController {
}
/// 跳过连接
void skipConnecting() async{
// 启动BLE后台服务
void skipConnecting() async {
// 启动BLE后台服务
await _startAgentService();
_isNavigatingToHome = true;
Get.offAllNamed(Routes.home);

109
local_plugins/agent_service/android/src/main/kotlin/com/yunqiinnovation/agent_service/AgentService.kt

@ -671,43 +671,8 @@ object AgentService : CoroutineScope {
"function_call" to functionCall.toString(),
"result" to functionCallResult.toString(),
))
// FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
metadata = metaStr;
val meta = JSONObject(metaStr)
// FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
val musicUrl = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
processMusicPlay(musicUrl, name, sgener)
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
if (url.isNotEmpty()) {
playlist.add(mapOf("url" to url, "name" to name, "sgener" to sgener))
}
}
if (playlist.isNotEmpty()) {
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
}
}
}
FileLogger.d(TAG, "mcp调用结果: $functionCallResult")
metadata = autoPlayMusic(functionCallResult);
}
}
)
@ -1052,20 +1017,66 @@ object AgentService : CoroutineScope {
}
}
}
/*
* 自动播放音乐
* */
fun autoPlayMusic(functionCallResult: JSONObject): String{
val metaStr = functionCallResult.optString("meta")
if (metaStr.isNotEmpty()) {
val meta = JSONObject(metaStr)
FileLogger.d(TAG, "mcp调用结果: $metaStr")
if (meta.has("card_music")) { //音乐卡片
val cardMusic = meta.getJSONObject("card_music")
FileLogger.d(TAG, "检查到音乐卡片: $cardMusic")
val id = cardMusic.optString("id", "")
val url = cardMusic.optString("url", "")
val name = cardMusic.optString("name", "")
val sgener = cardMusic.optString("sgener", "")
val image = cardMusic.optString("image", "")
processMusicPlay(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}else if (meta.has("card_musiclist")) { //音乐列表
val cardMusiclist = meta.getJSONObject("card_musiclist")
val musics = cardMusiclist.optJSONArray("musics")
if (musics != null) {
val playlist = mutableListOf<Map<String, String>>()
for (i in 0 until musics.length()) {
val item = musics.optJSONObject(i) ?: continue
val id = item.optString("id", "")
val url = item.optString("url", "")
val name = item.optString("name", "")
val sgener = item.optString("sgener", "")
val image = item.optString("image", "")
playlist.add(mapOf("id" to id,"url" to url, "title" to name, "artist" to sgener,"coverUrl" to image))
}
if (playlist.isNotEmpty()) {
FileLogger.w(TAG, "自动播放音乐列表 ${playlist}")
processMusicPlayList(playlist)
} else {
FileLogger.w(TAG, "card_musiclist 中没有有效的音乐条目")
}
} else {
FileLogger.w(TAG, "card_musiclist.musics 不是一个数组")
}
}
return metaStr;
}
return "";
}
/**
* 处理音乐播放
* 将ai返回的音乐连接 在后台启动播放
*
* @param musicUrl 图片文件路径
* @param musicId 音乐ID
* @param musicUrl 音乐文件路径
* @param name 歌曲名称,默认为空
* @param singer 歌手,默认为false
* @param singer 歌手名称,默认为空
* @return 是否成功开始处理
*/
fun processMusicPlay(musicUrl:String,name: String = "",singer:String = ""){
fun processMusicPlay(song: Map<String,String>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
FileLogger.i(TAG, "启动音乐服务 播放音乐 $musicUrl")
MusicServiceStarter.startServiceWithCommand(context, "play", url = musicUrl)
FileLogger.i(TAG, "启动音乐服务 播放音乐 $song")
MusicServiceStarter.startServiceWithCommand(context, command = "play", song = song)
}
/**
@ -1077,11 +1088,13 @@ object AgentService : CoroutineScope {
* @param singer 歌手,默认为false
* @return 是否成功开始处理
*/
fun processMusicPlayList(playlist:List<Map<String,String>>){
// 在其他 Service、BroadcastReceiver 或 Application 中调用
val urls = playlist.mapNotNull { it["url"] }.distinct()
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 $urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls = urls)
fun processMusicPlayList(songs:List<Map<String,String>>){
// 如果 playlist 中有有效的歌曲,开始播放
if (songs.isNotEmpty()) {
FileLogger.i(TAG, "启动音乐服务 播放音乐列表 ${songs}")
MusicServiceStarter.startServiceWithPlaylist(context, command = "playlist", songs = songs)
} else {
FileLogger.i(TAG, "音乐列表为空,未启动播放服务")
}
}
}

100
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt

@ -5,6 +5,7 @@ import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
import android.media.AudioManager
import android.media.MediaPlayer
import android.os.Build
@ -17,6 +18,8 @@ import androidx.core.app.NotificationCompat
import androidx.media.MediaBrowserServiceCompat
import androidx.media.session.MediaButtonReceiver
import com.yunqiinnovation.deepvoice_utils.FileLogger
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean
class MusicService : MediaBrowserServiceCompat() {
@ -29,7 +32,7 @@ class MusicService : MediaBrowserServiceCompat() {
private val isRunning = AtomicBoolean(false)
private var currentUrlInternal: String? = null
private var currentStatus = false
private var playlist = mutableListOf<String>()
private var playlist = mutableListOf<Map<String,String>>()
private var currentIndex = 0
fun isServiceRunning() = isRunning.get()
@ -46,7 +49,7 @@ class MusicService : MediaBrowserServiceCompat() {
FileLogger.init(this)
isRunning.set(true)
setupMediaSession()
startForeground(NOTIFICATION_ID, createNotification("等待播放…"))
startForeground(NOTIFICATION_ID, createNotification("等待播放","DeepSound",""))
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
FileLogger.i(TAG, "MediaService 启动完成")
}
@ -59,12 +62,34 @@ class MusicService : MediaBrowserServiceCompat() {
}
val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY
FileLogger.i(TAG, "收到命令: $command")
FileLogger.i(TAG, "liwei------------------- onStartCommand: $command")
when (command) {
"play" -> intent.getStringExtra("url")?.let { url ->
if (!playlist.contains(url)) playlist.add(url)
currentIndex = playlist.indexOf(url)
play(url)
"play" -> intent.getSerializableExtra("song")?.let { url ->
val songMap = intent.getSerializableExtra("song") as? Map<String, String>
songMap?.let { song ->
val songId = song["id"] ?: return@let
val existingIndex = playlist.indexOfFirst { it["id"] == songId }
if (existingIndex == -1) {
playlist.add(song)
currentIndex = playlist.size - 1
} else {
currentIndex = existingIndex
}
play(song)
}
}
"playlist" -> {
@Suppress("UNCHECKED_CAST")
val list = intent.getSerializableExtra("songs") as? ArrayList<HashMap<String, String>>
list?.let {
playlist.clear()
playlist.addAll(it)
currentIndex = 0
play(playlist[currentIndex])
}
}
"pause" -> pause()
"resume" -> resume()
@ -106,7 +131,7 @@ class MusicService : MediaBrowserServiceCompat() {
setSessionToken(mediaSession.sessionToken)
}
private fun createNotification(contentText: String): Notification {
private fun createNotification(title: String,artist: String,coverUrl: String?): Notification {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
manager.createNotificationChannel(
@ -124,9 +149,9 @@ class MusicService : MediaBrowserServiceCompat() {
this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT
)
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("DeepSound")
.setContentText(contentText)
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle(title)
.setContentText(artist)
.setSmallIcon(android.R.drawable.ic_media_play)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setStyle(
@ -142,7 +167,31 @@ class MusicService : MediaBrowserServiceCompat() {
)
.addAction(android.R.drawable.ic_media_next, "下一首", nextIntent)
.setOngoing(currentStatus)
.build()
// 异步加载封面图
if (!coverUrl.isNullOrEmpty()) {
Thread {
try {
val url = URL(coverUrl)
val connection = url.openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val inputStream = connection.inputStream
val bitmap = BitmapFactory.decodeStream(inputStream)
// 设置大图标并更新通知
builder.setLargeIcon(bitmap)
val builtNotification = builder.build()
manager.notify(NOTIFICATION_ID, builtNotification)
} catch (e: Exception) {
e.printStackTrace()
manager.notify(NOTIFICATION_ID, builder.build())
}
}.start()
}
return builder.build()
}
/**
@ -171,7 +220,12 @@ class MusicService : MediaBrowserServiceCompat() {
}
}
private fun play(url: String) {
private fun play(song: Map<String,String>) {
val url = song["url"] ?: return
FileLogger.i(TAG, "开始播放: $url")
val title = song["title"] ?: "未知标题"
val artist = song["artist"] ?: "未知歌手"
val coverUrl = song["coverUrl"]
mediaPlayer?.release()
currentStatus = true
currentUrlInternal = url
@ -180,7 +234,7 @@ class MusicService : MediaBrowserServiceCompat() {
setAudioStreamType(AudioManager.STREAM_MUSIC)
setOnPreparedListener {
start()
updateNotification("正在播放")
updateNotification(title,artist,coverUrl)
FileLogger.i(TAG, "开始播放: $url")
}
setOnCompletionListener { playNextOrStop() }
@ -202,7 +256,11 @@ class MusicService : MediaBrowserServiceCompat() {
.setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f)
.build()
)
updateNotification("已暂停")
val song = playlist[currentIndex]
val title = song["title"] ?: "未知标题"
val artist = song["artist"] ?: "未知歌手"
val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PLAYING);
FileLogger.i(TAG, "已暂停")
@ -218,9 +276,13 @@ class MusicService : MediaBrowserServiceCompat() {
.setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f)
.build()
)
updateNotification("继续播放")
val song = playlist[currentIndex]
val title = song["title"] ?: "未知标题"
val artist = song["artist"] ?: "未知歌手"
val coverUrl = song["coverUrl"]
updateNotification(title,artist,coverUrl)
setPlaybackState(PlaybackStateCompat.STATE_PAUSED);
FileLogger.i(TAG, "继续播放")
// FileLogger.i(TAG, "继续播放")
}
}
@ -240,8 +302,8 @@ class MusicService : MediaBrowserServiceCompat() {
else stopSelf()
}
private fun updateNotification(contentText: String) {
val notification = createNotification(contentText)
private fun updateNotification(title: String,artist: String,coverUrl: String?) {
val notification = createNotification(title, artist, coverUrl)
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
manager.notify(NOTIFICATION_ID, notification)

30
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServicePlugin.kt

@ -4,7 +4,7 @@ import android.content.Context
import android.content.Intent
import androidx.core.content.ContextCompat
import com.yunqiinnovation.deepvoice_utils.FileLogger
import com.yunqiinnovation.music_service.MusicServiceStarter.startServiceWithCommand
import com.yunqiinnovation.music_service.MusicServiceStarter
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
@ -27,30 +27,38 @@ class MusicServicePlugin: FlutterPlugin, MethodCallHandler {
FileLogger.d(TAG, "Method call: ${call.method}")
when (call.method) {
"play" -> {
val url = call.argument<String>("url")
startServiceWithCommand(context, "play", url)
result.success(null)
val song = call.argument<Map<String, String>>("song")
if (song != null) {
MusicServiceStarter.startServiceWithCommand(context, "play", song)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing 'song' map", null)
}
}
"playlist" -> {
val urls = call.argument<List<String>>("urls")
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", urls)
result.success(null)
val songs = call.argument<List<Map<String, String>>>("songs")
if (songs != null && songs.isNotEmpty()) {
MusicServiceStarter.startServiceWithPlaylist(context, "playlist", songs)
result.success(null)
} else {
result.error("INVALID_ARGUMENT", "Missing or empty 'songs' list", null)
}
}
"pause" -> {
startServiceWithCommand(context, "pause")
MusicServiceStarter.startServiceWithCommand(context, "pause")
result.success(null)
}
"resume" -> {
startServiceWithCommand(context, "resume")
MusicServiceStarter.startServiceWithCommand(context, "resume")
result.success(null)
}
"stop" -> {
startServiceWithCommand(context, "stop")
MusicServiceStarter.startServiceWithCommand(context, "stop")
result.success(null)
}
"setVolume" -> {
val volume = call.argument<Double>("volume")?.toFloat()
startServiceWithCommand(context, "setVolume", volume = volume)
MusicServiceStarter.startServiceWithCommand(context, "setVolume", volume = volume)
result.success(null)
}
"getStatus" -> {

36
local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicServiceStarter.kt

@ -5,26 +5,48 @@ import android.content.Intent
import android.util.Log
import androidx.core.content.ContextCompat
//@Parcelize
//data class Song(
// val url: String,
// val title: String,
// val artist: String,
// val coverUrl: String?
//)
object MusicServiceStarter {
private val TAG = "MusicServiceStarter"
//播放单曲
fun startServiceWithCommand(context: Context, command: String, url: String? = null, volume: Float? = null) {
Log.i(TAG, "startServiceWithCommand: $command $url")
fun startServiceWithCommand(context: Context, command: String, song: Map<String,String>? = null, volume: Float? = null) {
Log.i(TAG, "liwei------------------- startServiceWithCommand: $command $song")
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
url?.let { putExtra("url", it) }
song?.let {
putExtra("song", HashMap(it)) // 只有非 null 时才 putExtra
}
volume?.let { putExtra("volume", it) }
}
ContextCompat.startForegroundService(context, intent)
val success = try {
ContextCompat.startForegroundService(context, intent)
true
} catch (e: Exception) {
Log.e("MusicServiceStarter", "liwei------------------- Failed to start service", e)
false
}
Log.d(" MusicServiceStarter", "liwei------------------- Service start success: $success")
}
//启动播放列表
fun startServiceWithPlaylist(context: Context,command: String, urls: List<String>?,volume: Float? = null) {
Log.i(TAG, "startServiceWithPlaylist: $urls")
fun startServiceWithPlaylist(context: Context,command: String, songs: List<Map<String,String>>,volume: Float? = null) {
Log.i(TAG, "liwei------------------- startServiceWithPlaylist: $command $songs")
val serializableList = ArrayList<HashMap<String, String>>().apply {
songs.forEach { add(HashMap(it)) }
}
val intent = Intent(context, MusicService::class.java).apply {
putExtra("command", command)
putStringArrayListExtra("urls", ArrayList(urls!!)) // 注意类型是 ArrayList<String>
putExtra("songs", serializableList) // ✅ Serializable
volume?.let { putExtra("volume", it) }
}
ContextCompat.startForegroundService(context, intent)
}
}

4
local_plugins/music_service/lib/music_service.dart

@ -39,8 +39,8 @@ class MusicService {
_statusStream = _statusEventChannel.receiveBroadcastStream();
}
Future<void> playMusic(String url) async {
await _methodChannel.invokeMethod('play', {'url': url});
Future<void> playMusic(Map<String, String> song) async {
await _methodChannel.invokeMethod('play', {'song': song});
}
Future<void> pauseMusic() async => _methodChannel.invokeMethod('pause');

Loading…
Cancel
Save