feat: live lyrics from notification title, fix topbar overlap
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String>()
|
||||
|
||||
init {
|
||||
HearthNotificationListenerService.onMediaTitle = { pkg, title ->
|
||||
notificationTitles[pkg] = title
|
||||
pushCurrent()
|
||||
}
|
||||
}
|
||||
|
||||
private var callback: ((List<MediaInfo>) -> Unit)? = null
|
||||
private var registered = false
|
||||
private val controllers = mutableListOf<MediaController>()
|
||||
@@ -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) } ?: "",
|
||||
|
||||
Reference in New Issue
Block a user