feat: media card polish - per-index controls, cover jump, wrap, slide transition

This commit is contained in:
2026-08-16 20:13:47 +08:00
parent c82418f846
commit 8d10d9e5e9
4 changed files with 250 additions and 89 deletions
@@ -2,6 +2,7 @@ package top.yeij.hearth.media
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.media.MediaMetadata
import android.media.session.MediaController
@@ -13,11 +14,13 @@ import java.io.ByteArrayOutputStream
// 监听系统活跃媒体会话(支持多个:音乐/听书/视频同时在线),
// 元数据/播放状态变化时实时回调(切歌、暂停/播放即时推送);
// 提供播放控制(上一首/暂停播放/下一首)封面提取。
// 提供播放控制(上一首/暂停播放/下一首)封面提取与跳转播放界面
// Watch active media sessions (supports multiple: music/audiobook/video at once),
// push in real time on metadata/playback-state change (track switch, play/pause);
// provide transport controls (previous/play-pause/next) and cover extraction.
// provide transport controls (previous/play-pause/next), cover extraction and
// jumping to the playback UI.
class MediaSessionSource(context: Context) {
private val appContext = context.applicationContext
private val msm = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
// 通知监听组件作为授权凭据传入 addOnActiveSessionsChangedListener
@@ -99,23 +102,48 @@ class MediaSessionSource(context: Context) {
}
}
// 播放控制:作用于第一个(当前展示)controller
// Transport controls: act on the first (currently shown) controller
fun previous() {
controllers.firstOrNull()?.transportControls?.skipToPrevious()
Log.d(TAG, "previous")
// 播放控制:作用于指定索引的 controller(对应 H5 当前滑到的会话)
// Transport controls: act on the controller at the given index (matching the
// session the H5 card is currently showing)
fun previous(index: Int) {
controllers.getOrNull(index)?.transportControls?.skipToPrevious()
Log.d(TAG, "previous: index=$index")
}
fun next() {
controllers.firstOrNull()?.transportControls?.skipToNext()
Log.d(TAG, "next")
fun next(index: Int) {
controllers.getOrNull(index)?.transportControls?.skipToNext()
Log.d(TAG, "next: index=$index")
}
fun playPause() {
val c = controllers.firstOrNull() ?: return
fun playPause(index: Int) {
val c = controllers.getOrNull(index) ?: return
val playing = c.playbackState?.state == PlaybackState.STATE_PLAYING
if (playing) c.transportControls.pause() else c.transportControls.play()
Log.d(TAG, "playPause: playing=$playing")
Log.d(TAG, "playPause: index=$index playing=$playing")
}
// 跳转到指定会话的播放界面:优先 sessionActivityPendingIntent),
// 失败则回退到启动该 App
// Jump to the session's playback UI: prefer sessionActivity (PendingIntent),
// fall back to launching the app
fun openMediaApp(index: Int) {
val c = controllers.getOrNull(index) ?: return
val sessionActivity = c.sessionActivity
if (sessionActivity != null) {
try {
sessionActivity.send()
Log.d(TAG, "openMediaApp: sessionActivity sent for ${c.packageName}")
return
} catch (e: Exception) {
Log.w(TAG, "openMediaApp: sessionActivity failed: ${e.message}")
}
}
val launch = appContext.packageManager.getLaunchIntentForPackage(c.packageName)
if (launch != null) {
launch.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
appContext.startActivity(launch)
Log.d(TAG, "openMediaApp: launched ${c.packageName}")
}
}
// 更新 controller 列表:注销旧回调、注册新回调