feat: notification listener service and media source hardening
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
package top.yeij.hearth.media
|
||||
|
||||
import android.service.notification.NotificationListenerService
|
||||
|
||||
// 通知监听服务:空实现,仅作为「通知使用权」授权凭据,
|
||||
// 让 MediaSessionManager.addOnActiveSessionsChangedListener(cb, listenerComponent)
|
||||
// 能拿到活跃媒体会话(否则普通 APK 拿不到 MEDIA_CONTENT_CONTROL 系统权限)。
|
||||
// Notification listener service: empty implementation, only serves as the
|
||||
// "notification access" authorization credential so that
|
||||
// MediaSessionManager.addOnActiveSessionsChangedListener(cb, listenerComponent)
|
||||
// can receive active media sessions (a normal APK cannot hold the
|
||||
// MEDIA_CONTENT_CONTROL system permission).
|
||||
class HearthNotificationListenerService : NotificationListenerService()
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user