From c3ae2d6e52cbff1c4ab888c596400b312a1c1435 Mon Sep 17 00:00:00 2001 From: liwei1dao Date: Tue, 13 May 2025 10:29:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=9F=B3=E4=B9=90=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/modules/agent/views/message_bubble.dart | 4 +- .../android/src/main/AndroidManifest.xml | 31 +- .../music_service/MusicService.kt | 363 +++++++++--------- 3 files changed, 203 insertions(+), 195 deletions(-) diff --git a/lib/modules/agent/views/message_bubble.dart b/lib/modules/agent/views/message_bubble.dart index a5d347169..f1d939fc2 100644 --- a/lib/modules/agent/views/message_bubble.dart +++ b/lib/modules/agent/views/message_bubble.dart @@ -715,7 +715,7 @@ class MessageBubble extends GetView { children: [ Text( "${city} ${now["date"]}", - style: TextStyle( + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, @@ -734,7 +734,7 @@ class MessageBubble extends GetView { children: [ Text( "${now["temperature"]}°", - style: TextStyle( + style: const TextStyle( fontSize: 36, fontWeight: FontWeight.bold, color: Colors.white, diff --git a/local_plugins/music_service/android/src/main/AndroidManifest.xml b/local_plugins/music_service/android/src/main/AndroidManifest.xml index 334dcc16a..3469dab1e 100644 --- a/local_plugins/music_service/android/src/main/AndroidManifest.xml +++ b/local_plugins/music_service/android/src/main/AndroidManifest.xml @@ -1,24 +1,29 @@ - - - - - - - - + android:exported="true"> + + + + + + + + + + + + + + + diff --git a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt index fd09c5a7c..5cca58168 100644 --- a/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt +++ b/local_plugins/music_service/android/src/main/kotlin/com/yunqiinnovation/music_service/MusicService.kt @@ -1,20 +1,25 @@ package com.yunqiinnovation.music_service -import android.app.* +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager import android.content.Context import android.content.Intent import android.media.AudioManager import android.media.MediaPlayer import android.os.Build +import android.os.Bundle import android.os.IBinder -import android.util.Log -import androidx.core.app.NotificationCompat +import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat +import androidx.core.app.NotificationCompat +import androidx.media.MediaBrowserServiceCompat +import androidx.media.session.MediaButtonReceiver import com.yunqiinnovation.deepvoice_utils.FileLogger import java.util.concurrent.atomic.AtomicBoolean -class MusicService : Service() { +class MusicService : MediaBrowserServiceCompat() { companion object { private const val TAG = "MusicService" @@ -22,242 +27,240 @@ class MusicService : Service() { private const val NOTIFICATION_ID = 1002 private val isRunning = AtomicBoolean(false) - private var currentStatus: Boolean = false private var currentUrlInternal: String? = null + private var currentStatus = false + private var playlist = mutableListOf() + private var currentIndex = 0 - - fun isServiceRunning(): Boolean = isRunning.get() - fun isPlaying(): Boolean = currentStatus + fun isServiceRunning() = isRunning.get() + fun isPlaying() = currentStatus fun getCurrentUrl(): String? = currentUrlInternal } + private lateinit var mediaSession: MediaSessionCompat + private lateinit var audioManager: AudioManager private var mediaPlayer: MediaPlayer? = null - private val playlist = mutableListOf() // 可选:初始化为预置歌曲 - private var currentIndex = 0 override fun onCreate() { super.onCreate() FileLogger.init(this) - FileLogger.i(TAG, "MusicService 正在创建...") isRunning.set(true) - mediaSession = MediaSessionCompat(this, TAG).apply { - setCallback(object : MediaSessionCompat.Callback() { - override fun onPlay() { - resume() - } - - override fun onPause() { - pause() - } - - override fun onSkipToNext() { - if (currentIndex + 1 < playlist.size) { - currentIndex++ - play(playlist[currentIndex]) - } - } - - override fun onSkipToPrevious() { - if (currentIndex - 1 >= 0) { - currentIndex-- - play(playlist[currentIndex]) - } - } - - override fun onStop() { - stopSelf() - } - }) - isActive = true - } - startForeground(NOTIFICATION_ID, createNotification("等待播放...")) - Log.i(TAG, "MusicService Created") + setupMediaSession() + startForeground(NOTIFICATION_ID, createNotification("等待播放…")) + audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager + FileLogger.i(TAG, "MediaService 启动完成") } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY - Log.i(TAG, "onStartCommand: $command" ) + intent?.action?.takeIf { it == Intent.ACTION_MEDIA_BUTTON }?.let { + // 分发给 MediaSession 处理媒体按钮 + MediaButtonReceiver.handleIntent(mediaSession, intent) + return START_STICKY + } + val command = intent?.getStringExtra("command") ?: return START_NOT_STICKY + FileLogger.i(TAG, "收到命令: $command") when (command) { - "play" -> { - val url = intent.getStringExtra("url") ?: return START_NOT_STICKY + "play" -> intent.getStringExtra("url")?.let { url -> if (!playlist.contains(url)) playlist.add(url) currentIndex = playlist.indexOf(url) play(url) } - "playlist" -> { - val urls = intent.getStringArrayListExtra("urls")?.filter { it.isNotEmpty() } ?: return START_NOT_STICKY - Log.i(TAG, "后台服务播放 playlist: $urls" ) - if (urls.isNotEmpty()) { - playlist.clear() - playlist.addAll(urls) - currentIndex = 0 - play(playlist[currentIndex]) - } else { - Log.w(TAG, "playlist 命令收到空列表") - } - } "pause" -> pause() "resume" -> resume() "stop" -> stopSelf() - "next" -> { - if (currentIndex + 1 < playlist.size) { - currentIndex++ - play(playlist[currentIndex]) - } - } - "previous" -> { - if (currentIndex - 1 >= 0) { - currentIndex-- - play(playlist[currentIndex]) - } - } - "setVolume" -> { - val volume = intent.getFloatExtra("volume", 1.0f).coerceIn(0f, 1f) - mediaPlayer?.setVolume(volume, volume) - } + "next" -> if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex]) + "previous" -> if (currentIndex - 1 >= 0) play(playlist[--currentIndex]) } - return START_STICKY } + private fun setupMediaSession() { + mediaSession = MediaSessionCompat(this, "MusicMediaSession").apply { + setFlags( + MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or + MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS + ) + // 回调中处理具体操作 + setCallback(object : MediaSessionCompat.Callback() { + override fun onPlay() { FileLogger.i(TAG, "onPlay 通知!"); resume()} + override fun onPause() { FileLogger.i(TAG, "onPause 通知!"); pause()} + override fun onSkipToNext(){ FileLogger.i(TAG, "onSkipToNext 通知!"); playNext()} + override fun onSkipToPrevious() { FileLogger.i(TAG, "onSkipToNext 通知!"); playPrevious()} + }) + // 初始播放状态 + setPlaybackState( + PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + ) + .setState(PlaybackStateCompat.STATE_STOPPED, 0, 1.0f) + .build() + ) + isActive = true + } + // 将 token 暴露给 MediaBrowserServiceCompat + setSessionToken(mediaSession.sessionToken) + } + + private fun createNotification(contentText: String): Notification { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + manager.createNotificationChannel( + NotificationChannel(CHANNEL_ID, "播放控制", NotificationManager.IMPORTANCE_LOW) + ) + } + + val prevIntent = MediaButtonReceiver.buildMediaButtonPendingIntent( + this, PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS + ) + val playPauseIntent = MediaButtonReceiver.buildMediaButtonPendingIntent( + this, if (currentStatus) PlaybackStateCompat.ACTION_PAUSE else PlaybackStateCompat.ACTION_PLAY + ) + val nextIntent = MediaButtonReceiver.buildMediaButtonPendingIntent( + this, PlaybackStateCompat.ACTION_SKIP_TO_NEXT + ) + + return NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle("DeepSound") + .setContentText(contentText) + .setSmallIcon(android.R.drawable.ic_media_play) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setStyle( + androidx.media.app.NotificationCompat.MediaStyle() + .setMediaSession(mediaSession.sessionToken) + .setShowActionsInCompactView(0, 1, 2) + ) + .addAction(android.R.drawable.ic_media_previous, "上一首", prevIntent) + .addAction( + if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play, + if (currentStatus) "暂停" else "播放", + playPauseIntent + ) + .addAction(android.R.drawable.ic_media_next, "下一首", nextIntent) + .setOngoing(currentStatus) + .build() + } + + /** + * 设置播放状态 + */ + private fun setPlaybackState(state: Int) { + val playbackStateBuilder = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SKIP_TO_NEXT or + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or + PlaybackStateCompat.ACTION_STOP or + PlaybackStateCompat.ACTION_SEEK_TO or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_REWIND + ) + // 使用更高的播放速度,让系统认为是真正的播放 + .setState(state, 0, 1.0f, System.currentTimeMillis()) + mediaSession.setPlaybackState(playbackStateBuilder.build()) + + // 确保MediaSession激活 + if (!mediaSession.isActive) { + mediaSession.isActive = true + } + } + private fun play(url: String) { - Log.i(TAG, "播放音乐: $url") - releaseMediaPlayer() - currentStatus = true; - currentUrlInternal = url; + mediaPlayer?.release() + currentStatus = true + currentUrlInternal = url mediaPlayer = MediaPlayer().apply { setDataSource(url) setAudioStreamType(AudioManager.STREAM_MUSIC) setOnPreparedListener { start() - currentStatus = true updateNotification("正在播放") - Log.i(TAG, "开始播放") - } - setOnCompletionListener { - Log.i(TAG, "播放完成") - // 播放下一首 - if (currentIndex + 1 < playlist.size) { - currentIndex++ - play(playlist[currentIndex]) // 播放下一首 - } else { - currentStatus = false - stopSelf() // 如果没有更多歌曲了,可以停止服务 - } + FileLogger.i(TAG, "开始播放: $url") } + setOnCompletionListener { playNextOrStop() } setOnErrorListener { _, what, extra -> - Log.e(TAG, "播放错误: what=$what extra=$extra") - currentStatus = false - stopSelf() + FileLogger.e(TAG, "播放错误 what=$what extra=$extra") true } prepareAsync() } + setPlaybackState(PlaybackStateCompat.STATE_PAUSED); } private fun pause() { - mediaPlayer?.let { - if (it.isPlaying) { - it.pause() - currentStatus= false - updateNotification("已暂停") - Log.i(TAG, "暂停播放") - } + mediaPlayer?.takeIf { it.isPlaying }?.apply { + pause() + currentStatus = false + mediaSession.setPlaybackState( + PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PAUSED, currentPosition.toLong(), 1.0f) + .build() + ) + updateNotification("已暂停") + setPlaybackState(PlaybackStateCompat.STATE_PLAYING); + FileLogger.i(TAG, "已暂停") + } } private fun resume() { - mediaPlayer?.let { - if (!it.isPlaying) { - it.start() - currentStatus = true - updateNotification("继续播放") - Log.i(TAG, "继续播放") - } + mediaPlayer?.takeIf { !it.isPlaying }?.apply { + start() + currentStatus = true + mediaSession.setPlaybackState( + PlaybackStateCompat.Builder() + .setState(PlaybackStateCompat.STATE_PLAYING, currentPosition.toLong(), 1.0f) + .build() + ) + updateNotification("继续播放") + setPlaybackState(PlaybackStateCompat.STATE_PAUSED); + FileLogger.i(TAG, "继续播放") } } - private fun releaseMediaPlayer() { - mediaPlayer?.apply { - stop() - release() - } - mediaPlayer = null + private fun playNext() { + if (playlist.isEmpty()) return + currentIndex = (currentIndex + 1) % playlist.size + play(playlist[currentIndex]) } - private fun updateNotification(contentText: String) { - val notification = createNotification(contentText) - val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - manager.notify(NOTIFICATION_ID, notification) + private fun playPrevious() { + if (playlist.isEmpty()) return + currentIndex = if (currentIndex - 1 < 0) playlist.size - 1 else currentIndex - 1 + play(playlist[currentIndex]) + } + private fun playNextOrStop() { + if (currentIndex + 1 < playlist.size) play(playlist[++currentIndex]) + else stopSelf() } - private fun createNotification(contentText: String): Notification { + private fun updateNotification(contentText: String) { + val notification = createNotification(contentText) val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val channel = NotificationChannel( - CHANNEL_ID, - "音乐播放服务", - NotificationManager.IMPORTANCE_LOW - ) - manager.createNotificationChannel(channel) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + manager.notify(NOTIFICATION_ID, notification) + } else { + @Suppress("DEPRECATION") + startForeground(NOTIFICATION_ID, notification) } + } - // 修复 pendingIntent:打开 App 主 Activity - val activityIntent = packageManager.getLaunchIntentForPackage(packageName) - val pendingIntent = PendingIntent.getActivity( - this, - 0, - activityIntent, - PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - ) + override fun onGetRoot( + clientPackageName: String, clientUid: Int, rootHints: Bundle? + ) = BrowserRoot("root_id", null) - return NotificationCompat.Builder(this, CHANNEL_ID) - .setContentTitle("DeepSound") - .setContentText(contentText) - .setSmallIcon(android.R.drawable.ic_media_play) - .setContentIntent(pendingIntent) - .setOngoing(true) - .setStyle( - androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(mediaSession.sessionToken) - .setShowActionsInCompactView(0, 1, 2) - ) - .addAction( - NotificationCompat.Action( - android.R.drawable.ic_media_previous, "上一曲", - androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent( - this, - PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS - ) - ) - ) - .addAction( - NotificationCompat.Action( - if (currentStatus) android.R.drawable.ic_media_pause else android.R.drawable.ic_media_play, - if (currentStatus) "暂停" else "播放", - androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent( - this, - PlaybackStateCompat.ACTION_PLAY_PAUSE - ) - ) - ) - .addAction( - NotificationCompat.Action( - android.R.drawable.ic_media_next, "下一曲", - androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent( - this, - PlaybackStateCompat.ACTION_SKIP_TO_NEXT - ) - ) - ) - .build() - } + override fun onLoadChildren( + parentId: String, result: Result> + ) = result.sendResult(mutableListOf()) override fun onDestroy() { - Log.i(TAG, "服务销毁,释放播放器") - releaseMediaPlayer() + mediaPlayer?.release() stopForeground(true) isRunning.set(false) super.onDestroy()