Files
Hearth/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt
T

99 lines
4.7 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package top.yeij.hearth.media
import android.content.ComponentName
import android.content.Context
import android.media.MediaMetadata
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.media.session.PlaybackState
import android.util.Log
// 监听系统活跃媒体会话,取第一个 controller 的 metadata + playbackState 构造 MediaInfo 回调
// Watch active media sessions; build MediaInfo from the first controller's metadata + playbackState
class MediaSessionSource(context: Context) {
private val msm = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
// 通知监听组件作为授权凭据传入 addOnActiveSessionsChangedListener
// 取代依赖 MEDIA_CONTENT_CONTROL 系统权限的 null 方案。
// The notification listener component is passed as the authorization credential,
// replacing the null-based approach that relied on the MEDIA_CONTENT_CONTROL permission.
private val listenerComponent = ComponentName(context, HearthNotificationListenerService::class.java)
private var callback: ((MediaInfo?) -> Unit)? = null
private var registered = false
// 具名监听器字段:会话列表变化时回调
// Named listener field: fires when the active-session list changes
private val activeSessionsListener =
MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
val cb = callback ?: return@OnActiveSessionsChangedListener
notifyMedia(controllers?.firstOrNull(), cb)
}
// 注册监听:幂等——先移除旧监听再注册,避免重复 start 累积监听器。
// 注册后主动拉取一次当前活跃会话:注册前已开始的播放不会触发变化回调,
// 主动拉取保证启动时媒体卡即可显示。
// Register the listener: idempotent — remove the old one first to avoid accumulation.
// After registering, actively pull the current sessions once: playback that started
// before registration never fires the change callback, so the active pull makes the
// media card show immediately on startup.
fun start(cb: (MediaInfo?) -> Unit) {
stop()
callback = cb
try {
msm.addOnActiveSessionsChangedListener(activeSessionsListener, listenerComponent)
registered = true
Log.d(TAG, "start: media session listener registered")
val current = msm.getActiveSessions(listenerComponent).firstOrNull()
notifyMedia(current, cb)
} catch (e: SecurityException) {
// 未授权「通知使用权」时系统抛 SecurityException:媒体监听降级为不可用,不崩溃。
// 用户在系统设置开启通知使用权后,重启 Hearth 才会恢复媒体卡。
// MediaSessionService throws SecurityException without notification access;
// degrade to media listening disabled instead of crashing. Media card recovers
// after the user grants notification access and restarts Hearth.
Log.w(TAG, "start: no notification access, media listener disabled: ${e.message}")
registered = false
}
}
// 注销监听:释放回调引用,供 Activity onDestroy 调用
// Unregister the listener: release the callback, call from Activity onDestroy
fun stop() {
if (registered) {
msm.removeOnActiveSessionsChangedListener(activeSessionsListener)
registered = false
Log.d(TAG, "stop: media session listener removed")
}
callback = null
}
// 从 controller 提取 metadata + playbackState 构造 MediaInfo 并回调
// Build MediaInfo from the controller's metadata + playbackState and invoke the callback
private fun notifyMedia(controller: MediaController?, cb: (MediaInfo?) -> Unit) {
if (controller == null) {
Log.d(TAG, "no active media session")
cb(null)
return
}
val meta = controller.metadata
val state = controller.playbackState
cb(
MediaInfo(
meta?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "",
meta?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "",
meta?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "",
state?.state == PlaybackState.STATE_PLAYING,
state?.position ?: 0L,
meta?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0L,
controller.packageName,
)
)
Log.d(TAG, "media: ${controller.packageName}")
}
companion object {
private const val TAG = "HearthMedia"
}
}