99 lines
4.7 KiB
Kotlin
99 lines
4.7 KiB
Kotlin
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"
|
||
}
|
||
}
|