feat: notification listener service and media source hardening
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
|
||||||
<uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL" />
|
|
||||||
<application
|
<application
|
||||||
android:label="Hearth"
|
android:label="Hearth"
|
||||||
android:theme="@android:style/Theme.Material.NoActionBar">
|
android:theme="@android:style/Theme.Material.NoActionBar">
|
||||||
@@ -16,5 +15,14 @@
|
|||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
<service
|
||||||
|
android:name=".media.HearthNotificationListenerService"
|
||||||
|
android:label="Hearth 媒体监听"
|
||||||
|
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
|
||||||
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.notification.NotificationListenerService" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -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
|
package top.yeij.hearth.media
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaMetadata
|
import android.media.MediaMetadata
|
||||||
import android.media.session.MediaController
|
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
|
// Watch active media sessions; build MediaInfo from the first controller's metadata + playbackState
|
||||||
class MediaSessionSource(context: Context) {
|
class MediaSessionSource(context: Context) {
|
||||||
private val msm = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
private val msm = context.getSystemService(Context.MEDIA_SESSION_SERVICE) as MediaSessionManager
|
||||||
private var listener: ((MediaInfo?) -> Unit)? = null
|
|
||||||
|
|
||||||
fun start(cb: (MediaInfo?) -> Unit) {
|
// 通知监听组件作为授权凭据传入 addOnActiveSessionsChangedListener,
|
||||||
listener = cb
|
// 取代依赖 MEDIA_CONTENT_CONTROL 系统权限的 null 方案。
|
||||||
msm.addOnActiveSessionsChangedListener({ controllers ->
|
// 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()
|
val c = controllers?.firstOrNull()
|
||||||
if (c == null) {
|
if (c == null) {
|
||||||
Log.d("HearthMedia", "no active media session")
|
Log.d(TAG, "no active media session")
|
||||||
cb(null)
|
cb(null)
|
||||||
return@addOnActiveSessionsChangedListener
|
return@OnActiveSessionsChangedListener
|
||||||
}
|
}
|
||||||
val meta = c.metadata
|
val meta = c.metadata
|
||||||
val state = c.playbackState
|
val state = c.playbackState
|
||||||
@@ -35,7 +46,32 @@ class MediaSessionSource(context: Context) {
|
|||||||
c.packageName,
|
c.packageName,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Log.d("HearthMedia", "media: ${c.packageName}")
|
Log.d(TAG, "media: ${c.packageName}")
|
||||||
}, null)
|
}
|
||||||
|
|
||||||
|
// 注册监听:幂等——先移除旧监听再注册,避免重复 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