feat: notification listener service and media source hardening

This commit is contained in:
2026-08-16 17:02:11 +08:00
parent 05a4009532
commit abed4f4e15
3 changed files with 66 additions and 9 deletions
@@ -1,5 +1,6 @@
package top.yeij.hearth.media
import android.content.ComponentName
import android.content.Context
import android.media.MediaMetadata
import android.media.session.MediaController
@@ -11,16 +12,26 @@ import android.util.Log
// 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
private var listener: ((MediaInfo?) -> Unit)? = null
fun start(cb: (MediaInfo?) -> Unit) {
listener = cb
msm.addOnActiveSessionsChangedListener({ controllers ->
// 通知监听组件作为授权凭据传入 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
// 具名监听器字段:回调通过 callback 字段转发,便于 start/stop 幂等管理
// Named listener field: forwards via callback, enabling idempotent start/stop
private val activeSessionsListener =
MediaSessionManager.OnActiveSessionsChangedListener { controllers ->
val cb = callback ?: return@OnActiveSessionsChangedListener
val c = controllers?.firstOrNull()
if (c == null) {
Log.d("HearthMedia", "no active media session")
Log.d(TAG, "no active media session")
cb(null)
return@addOnActiveSessionsChangedListener
return@OnActiveSessionsChangedListener
}
val meta = c.metadata
val state = c.playbackState
@@ -35,7 +46,32 @@ class MediaSessionSource(context: Context) {
c.packageName,
)
)
Log.d("HearthMedia", "media: ${c.packageName}")
}, null)
Log.d(TAG, "media: ${c.packageName}")
}
// 注册监听:幂等——先移除旧监听再注册,避免重复 start 累积监听器
// Register the listener: idempotent — remove the old listener first, avoiding
// listener accumulation from repeated start() calls
fun start(cb: (MediaInfo?) -> Unit) {
stop()
callback = cb
msm.addOnActiveSessionsChangedListener(activeSessionsListener, listenerComponent)
registered = true
Log.d(TAG, "start: media session listener registered")
}
// 注销监听:释放回调引用,供 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
}
companion object {
private const val TAG = "HearthMedia"
}
}