diff --git a/app/src/main/assets/h5/js/pages/settings.js b/app/src/main/assets/h5/js/pages/settings.js
index da8ccac..5944f30 100644
--- a/app/src/main/assets/h5/js/pages/settings.js
+++ b/app/src/main/assets/h5/js/pages/settings.js
@@ -1,5 +1,5 @@
-// 设置页:Miuix Preference 分组 + 玻璃条目(仅 UI 骨架,交互留后续)
-// Settings page: Miuix Preference groups + glass items (UI skeleton only; interactions later)
+// 设置页:Miuix Preference 分组 + 玻璃条目(通知使用权为可用交互项)
+// Settings page: Miuix Preference groups + glass items (notification access is interactive)
window.loadSettings = function () {
const page = document.querySelector('[data-page="settings"]');
if (page.dataset.loaded) return; page.dataset.loaded = '1';
@@ -7,10 +7,30 @@ window.loadSettings = function () {
显示
跟随系统主题
屏幕亮度
+ 权限
+
+ 通知使用权(媒体卡)
+ ›
+
网络
webAPP 服务器地址›
检查更新›
关于
版本 0.1.0›
`;
+ setupNotificationAccess(page);
};
+
+// 通知使用权授权项:异步查状态,未授权点击跳转系统授权页
+// Notification-access item: async state check, tap to open the system grant page
+function setupNotificationAccess(page) {
+ const item = page.querySelector('#notif-access');
+ const state = page.querySelector('#notif-access-state');
+ bridge.call('getNotificationAccess').then(granted => {
+ state.textContent = granted ? '已开启 ✓' : '去开启 ›';
+ if (granted) state.style.color = 'var(--accent)';
+ }).catch(() => {});
+ item.onclick = () => {
+ bridge.call('requestNotificationAccess').catch(() => {});
+ };
+}
diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt
index fcfd53f..ea5a112 100644
--- a/app/src/main/java/top/yeij/hearth/MainActivity.kt
+++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt
@@ -1,8 +1,10 @@
package top.yeij.hearth
import android.app.Activity
+import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
+import android.provider.Settings
import android.text.TextUtils
import android.util.Log
import android.view.View
@@ -24,6 +26,7 @@ import top.yeij.hearth.webapp.Tab
import top.yeij.hearth.webapp.WebAppContainer
import top.yeij.hearth.webapp.WebAppRepository
import top.yeij.hearth.webview.JsBridge
+import top.yeij.hearth.webview.NotificationAccessProvider
import top.yeij.hearth.webview.WebAppHost
import top.yeij.hearth.webview.WebViewManager
import java.io.File
@@ -47,6 +50,19 @@ class MainActivity : Activity() {
private val webAppContainer = WebAppContainer()
private val gson = Gson()
private val webAppHost = WebAppHostImpl()
+
+ // 通知使用权访问实现:检查授权状态 + 跳转系统授权页(供设置页授权项使用)
+ // Notification-access implementation: check grant state + jump to system settings
+ // (used by the settings permission item)
+ private val notificationAccess = object : NotificationAccessProvider {
+ override fun isGranted(): Boolean =
+ NotificationManagerCompat.getEnabledListenerPackages(this@MainActivity)
+ .contains(packageName)
+
+ override fun requestAccess() {
+ startActivity(Intent(Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS))
+ }
+ }
private lateinit var mediaSource: MediaSessionSource
private lateinit var root: FrameLayout
private lateinit var desktopWebView: WebView
@@ -91,6 +107,7 @@ class MainActivity : Activity() {
webAppContainer = webAppContainer,
webAppHost = webAppHost,
postToMainThread = { desktopWebView.post(it) },
+ notificationAccess = notificationAccess,
)
root = FrameLayout(this)
setContentView(root)
diff --git a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt
index 5bd9c9a..738e39d 100644
--- a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt
+++ b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt
@@ -10,6 +10,15 @@ import top.yeij.hearth.webapp.WebApp
import top.yeij.hearth.webapp.WebAppContainer
import top.yeij.hearth.webapp.WebAppRepository
+// 通知使用权访问接口:由 MainActivity 实现,JsBridge 通过它检查/申请授权,
+// 保持 JsBridge 可 JVM 单测(不直接依赖 Context/NotificationManagerCompat)
+// Notification-access provider implemented by MainActivity; JsBridge uses it to
+// check/request access, keeping JsBridge JVM-testable (no direct Context dependency)
+interface NotificationAccessProvider {
+ fun isGranted(): Boolean
+ fun requestAccess()
+}
+
class JsBridge(
private val deviceWidthPx: Int,
private val deviceHeightPx: Int,
@@ -23,6 +32,9 @@ class JsBridge(
// 主线程执行器:View 操作必须切到主线程;null(单测)时同步执行
// Main-thread executor: view ops must run on main; null (unit tests) runs inline
private val postToMainThread: ((() -> Unit) -> Unit)? = null,
+ // 通知使用权访问接口(检查/申请授权),供设置页授权项使用
+ // Notification-access provider (check/request) for the settings permission item
+ private val notificationAccess: NotificationAccessProvider? = null,
) {
private val gson = com.google.gson.Gson()
@@ -185,6 +197,19 @@ class JsBridge(
return gson.toJson(cards)
}
+ // 返回通知使用权是否已授权(未接入返回 false)
+ // Return whether notification access is granted (false when not wired)
+ @android.webkit.JavascriptInterface
+ fun getNotificationAccess(): Boolean = notificationAccess?.isGranted() ?: false
+
+ // 跳转系统「通知使用权」设置页,让用户为媒体卡授权
+ // Jump to the system notification-access settings page for media-card grant
+ @android.webkit.JavascriptInterface
+ fun requestNotificationAccess() {
+ Log.d("HearthBridge", "requestNotificationAccess called")
+ notificationAccess?.requestAccess()
+ }
+
// 注册媒体会话监听,回调推送给 H5(info 为 null 时推 "null")
// Register media session listener; push callbacks to H5 (push "null" when info is null)
fun setMediaListener(source: MediaSessionSource, webView: android.webkit.WebView) {