From abed4f4e154ed96bf66cb29f12fddce35105b9f2 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Sun, 16 Aug 2026 17:02:11 +0800 Subject: [PATCH] feat: notification listener service and media source hardening --- app/src/main/AndroidManifest.xml | 10 +++- .../HearthNotificationListenerService.kt | 13 +++++ .../yeij/hearth/media/MediaSessionSource.kt | 52 ++++++++++++++++--- 3 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6d5bc3e..78bec7d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,7 +1,6 @@ - @@ -16,5 +15,14 @@ + + + + + diff --git a/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt b/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt new file mode 100644 index 0000000..c6455f7 --- /dev/null +++ b/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt @@ -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() diff --git a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt index a3cc06f..5def390 100644 --- a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt +++ b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt @@ -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" } }