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" } }