diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index d3d059c..c7b3792 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -51,9 +51,11 @@ class MainActivity : Activity() { // Placeholder manifest/catalog URLs; replace with the real server on device private const val MANIFEST_URL = "https://example.com/hearth/manifest.json" private const val CATALOG_URL = "https://example.com/hearth/catalog.json" - // 内容 WebView 顶栏预留高度(dp) - // Topbar reserved height for content WebViews (dp) - private const val TOPBAR_HEIGHT_DP = 44 + // 内容 WebView 顶栏预留高度(dp):H5 顶栏 top 12px + 按钮 30px + padding,约 54dp, + // 取 56dp 留余量,避免内容 WebView 覆盖顶栏 + // Topbar reserved height (dp): H5 topbar top 12px + button 30px + padding ≈ 54dp; + // use 56dp with margin so the content WebView doesn't cover the topbar + private const val TOPBAR_HEIGHT_DP = 56 // 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏 // Global sidebar collapsed width (dp); content WebViews reserve it on the // left to avoid covering the sidebar diff --git a/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt b/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt index c6455f7..aa80d50 100644 --- a/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt +++ b/app/src/main/java/top/yeij/hearth/media/HearthNotificationListenerService.kt @@ -1,13 +1,34 @@ package top.yeij.hearth.media +import android.app.Notification import android.service.notification.NotificationListenerService +import android.service.notification.StatusBarNotification +import android.util.Log -// 通知监听服务:空实现,仅作为「通知使用权」授权凭据, -// 让 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() +// 通知监听服务:作为「通知使用权」授权凭据(让 getActiveSessions 可用), +// 同时监听媒体通知的实时标题变化(如网易云歌词实时显示在通知标题)。 +// Notification listener service: serves as the "notification access" credential +// (making getActiveSessions usable), and also watches media notification title +// changes (e.g. NetEase Cloud Music shows live lyrics in the notification title). +class HearthNotificationListenerService : NotificationListenerService() { + + companion object { + private const val TAG = "HearthNotifListener" + + // 媒体通知实时标题回调(packageName -> title),供 MediaSessionSource 订阅 + // Media notification live-title callback (packageName -> title), subscribed by MediaSessionSource + @Volatile + var onMediaTitle: ((packageName: String, title: String) -> Unit)? = null + } + + override fun onNotificationPosted(sbn: StatusBarNotification?) { + sbn ?: return + val n = sbn.notification ?: return + // 仅处理媒体通知(CATEGORY_TRANSPORT) + // Only handle media notifications (CATEGORY_TRANSPORT) + if (n.category != Notification.CATEGORY_TRANSPORT) return + val title = n.extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: return + Log.d(TAG, "onNotificationPosted: ${sbn.packageName} title=$title") + onMediaTitle?.invoke(sbn.packageName, title) + } +} 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 d14fe20..8009226 100644 --- a/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt +++ b/app/src/main/java/top/yeij/hearth/media/MediaSessionSource.kt @@ -31,6 +31,20 @@ class MediaSessionSource(context: Context) { // replacing the null-based approach that relied on the MEDIA_CONTENT_CONTROL permission. private val listenerComponent = ComponentName(context, HearthNotificationListenerService::class.java) + // 通知实时标题缓存(packageName -> title),来自通知栏的实时歌词, + // 优先于 MediaSession 的 METADATA_KEY_TITLE(歌词只在通知里更新) + // Live notification title cache (packageName -> title) from the notification + // shade; it takes priority over MediaSession METADATA_KEY_TITLE because lyrics + // only update in the notification + private val notificationTitles = mutableMapOf() + + init { + HearthNotificationListenerService.onMediaTitle = { pkg, title -> + notificationTitles[pkg] = title + pushCurrent() + } + } + private var callback: ((List) -> Unit)? = null private var registered = false private val controllers = mutableListOf() @@ -194,8 +208,12 @@ class MediaSessionSource(context: Context) { private fun toMediaInfo(c: MediaController): MediaInfo { val meta = c.metadata val state = c.playbackState + // 标题优先用通知实时标题(歌词),fallback 到 MediaSession 元数据 + // Title prefers the live notification title (lyrics), falling back to MediaSession metadata + val title = notificationTitles[c.packageName] + ?: (meta?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "") return MediaInfo( - meta?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "", + title, meta?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "", meta?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "", meta?.getBitmap(MediaMetadata.METADATA_KEY_ART)?.let { bitmapToBase64(it) } ?: "",