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
|
// 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 MANIFEST_URL = "https://example.com/hearth/manifest.json"
|
||||||
private const val CATALOG_URL = "https://example.com/hearth/catalog.json"
|
private const val CATALOG_URL = "https://example.com/hearth/catalog.json"
|
||||||
// 内容 WebView 顶栏预留高度(dp)
|
// 内容 WebView 顶栏预留高度(dp):H5 顶栏 top 12px + 按钮 30px + padding,约 54dp,
|
||||||
// Topbar reserved height for content WebViews (dp)
|
// 取 56dp 留余量,避免内容 WebView 覆盖顶栏
|
||||||
private const val TOPBAR_HEIGHT_DP = 44
|
// 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 左侧预留,避免挡住侧边栏
|
// 全局侧边栏收起宽度(dp),内容 WebView 左侧预留,避免挡住侧边栏
|
||||||
// Global sidebar collapsed width (dp); content WebViews reserve it on the
|
// Global sidebar collapsed width (dp); content WebViews reserve it on the
|
||||||
// left to avoid covering the sidebar
|
// left to avoid covering the sidebar
|
||||||
|
|||||||
@@ -1,13 +1,34 @@
|
|||||||
package top.yeij.hearth.media
|
package top.yeij.hearth.media
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
import android.service.notification.NotificationListenerService
|
import android.service.notification.NotificationListenerService
|
||||||
|
import android.service.notification.StatusBarNotification
|
||||||
|
import android.util.Log
|
||||||
|
|
||||||
// 通知监听服务:空实现,仅作为「通知使用权」授权凭据,
|
// 通知监听服务:作为「通知使用权」授权凭据(让 getActiveSessions 可用),
|
||||||
// 让 MediaSessionManager.addOnActiveSessionsChangedListener(cb, listenerComponent)
|
// 同时监听媒体通知的实时标题变化(如网易云歌词实时显示在通知标题)。
|
||||||
// 能拿到活跃媒体会话(否则普通 APK 拿不到 MEDIA_CONTENT_CONTROL 系统权限)。
|
// Notification listener service: serves as the "notification access" credential
|
||||||
// Notification listener service: empty implementation, only serves as the
|
// (making getActiveSessions usable), and also watches media notification title
|
||||||
// "notification access" authorization credential so that
|
// changes (e.g. NetEase Cloud Music shows live lyrics in the notification title).
|
||||||
// MediaSessionManager.addOnActiveSessionsChangedListener(cb, listenerComponent)
|
class HearthNotificationListenerService : NotificationListenerService() {
|
||||||
// can receive active media sessions (a normal APK cannot hold the
|
|
||||||
// MEDIA_CONTENT_CONTROL system permission).
|
companion object {
|
||||||
class HearthNotificationListenerService : NotificationListenerService()
|
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.
|
// replacing the null-based approach that relied on the MEDIA_CONTENT_CONTROL permission.
|
||||||
private val listenerComponent = ComponentName(context, HearthNotificationListenerService::class.java)
|
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 callback: ((List<MediaInfo>) -> Unit)? = null
|
||||||
private var registered = false
|
private var registered = false
|
||||||
private val controllers = mutableListOf<MediaController>()
|
private val controllers = mutableListOf<MediaController>()
|
||||||
@@ -194,8 +208,12 @@ class MediaSessionSource(context: Context) {
|
|||||||
private fun toMediaInfo(c: MediaController): MediaInfo {
|
private fun toMediaInfo(c: MediaController): MediaInfo {
|
||||||
val meta = c.metadata
|
val meta = c.metadata
|
||||||
val state = c.playbackState
|
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(
|
return MediaInfo(
|
||||||
meta?.getString(MediaMetadata.METADATA_KEY_TITLE) ?: "",
|
title,
|
||||||
meta?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "",
|
meta?.getString(MediaMetadata.METADATA_KEY_ARTIST) ?: "",
|
||||||
meta?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "",
|
meta?.getString(MediaMetadata.METADATA_KEY_ALBUM) ?: "",
|
||||||
meta?.getBitmap(MediaMetadata.METADATA_KEY_ART)?.let { bitmapToBase64(it) } ?: "",
|
meta?.getBitmap(MediaMetadata.METADATA_KEY_ART)?.let { bitmapToBase64(it) } ?: "",
|
||||||
|
|||||||
Reference in New Issue
Block a user