From 381b9a57e703de13259775dc5743ec9759bbd280 Mon Sep 17 00:00:00 2001
From: AskaEth
Date: Sun, 16 Aug 2026 17:03:07 +0800
Subject: [PATCH] feat: wire MainActivity with repositories, error page, and
back handling
---
.../main/java/top/yeij/hearth/MainActivity.kt | 107 ++++++++++++++++--
.../yeij/hearth/webapp/OkHttpHttpClient.kt | 28 +++++
2 files changed, 126 insertions(+), 9 deletions(-)
create mode 100644 app/src/main/java/top/yeij/hearth/webapp/OkHttpHttpClient.kt
diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt
index 217f6fa..a3ae062 100644
--- a/app/src/main/java/top/yeij/hearth/MainActivity.kt
+++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt
@@ -3,44 +3,90 @@ package top.yeij.hearth
import android.app.Activity
import android.content.res.Configuration
import android.os.Bundle
+import android.text.TextUtils
import android.util.Log
import android.view.WindowInsets
import android.view.WindowInsetsController
+import android.webkit.WebView
+import android.webkit.WebViewClient
+import androidx.core.app.NotificationManagerCompat
import top.yeij.hearth.app.AndroidAppSource
import top.yeij.hearth.app.AppRepository
+import top.yeij.hearth.cache.CacheManager
+import top.yeij.hearth.card.CardRepository
+import top.yeij.hearth.media.MediaSessionSource
+import top.yeij.hearth.webapp.OkHttpHttpClient
+import top.yeij.hearth.webapp.WebAppContainer
+import top.yeij.hearth.webapp.WebAppRepository
import top.yeij.hearth.webview.JsBridge
import top.yeij.hearth.webview.WebViewManager
+import java.io.File
-// HOME 桌面 activity,后续 Task 挂载 WebView 与 JsBridge
-// HOME launcher activity; later tasks mount WebView and JsBridge
+// HOME 桌面 activity,接线各能力层:Repository、媒体监听、WebView 错误页与 Back 键
+// HOME launcher activity, wiring all capability layers: repositories, media listener,
+// WebView error page, and Back key handling
class MainActivity : Activity() {
companion object {
private const val TAG = "HearthMainActivity"
+ // 占位清单/目录地址,真机部署时可替换为实际服务器
+ // 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"
}
+ private val webAppContainer = WebAppContainer()
+ private lateinit var mediaSource: MediaSessionSource
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.d(TAG, "onCreate: enter immersive mode")
enterImmersive()
setupWebView()
+ checkNotificationAccess()
}
- // 创建 JsBridge 并挂载 WebView 桌面层
- // Create JsBridge and mount the WebView launcher layer
+ override fun onDestroy() {
+ mediaSource.stop()
+ super.onDestroy()
+ }
+
+ // 创建 JsBridge 并挂载 WebView 桌面层,接线所有 Repository 与媒体监听
+ // Create JsBridge and mount the WebView launcher layer, wiring all repositories
+ // and the media session listener
private fun setupWebView() {
val dm = resources.displayMetrics
val darkMode =
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
Configuration.UI_MODE_NIGHT_YES
+ val cache = CacheManager(File(filesDir, "cache"))
+ val http = OkHttpHttpClient()
val bridge = JsBridge(
deviceWidthPx = dm.widthPixels,
deviceHeightPx = dm.heightPixels,
density = dm.density,
darkMode = darkMode,
appRepository = AppRepository(AndroidAppSource(this)),
+ webAppRepository = WebAppRepository(http, cache, MANIFEST_URL),
+ cardRepository = CardRepository(http, cache, CATALOG_URL),
)
- WebViewManager(this).attach(bridge)
+ val webView = WebViewManager(this).attach(bridge)
+ webView.webViewClient = object : WebViewClient() {
+ // 主帧加载失败时展示错误页,避免白屏
+ // Show an error page on main-frame load failure to avoid a blank screen
+ @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
+ override fun onReceivedError(
+ view: WebView?,
+ code: Int,
+ description: String?,
+ failingUrl: String?,
+ ) {
+ Log.d(TAG, "onReceivedError: code=$code desc=$description url=$failingUrl")
+ view?.loadDataWithBaseURL(null, errorPage(description ?: "未知错误"), "text/html", "utf-8", null)
+ }
+ }
+ mediaSource = MediaSessionSource(this)
+ bridge.setMediaListener(mediaSource, webView)
Log.d(
TAG,
"setupWebView: attached WebView ${dm.widthPixels}x${dm.heightPixels}" +
@@ -48,6 +94,36 @@ class MainActivity : Activity() {
)
}
+ // 检测通知使用权是否已授予,未授予时打日志提示(UI 引导入口留后续)
+ // Check whether notification access is granted; log a hint when not
+ // (the settings UI entry is deferred to a later task)
+ private fun checkNotificationAccess() {
+ val granted = NotificationManagerCompat.getEnabledListenerPackages(this).contains(packageName)
+ Log.d(TAG, "checkNotificationAccess: notification listener enabled=$granted")
+ if (!granted) {
+ Log.d(
+ TAG,
+ "checkNotificationAccess: grant via Settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"
+ )
+ }
+ }
+
+ // 构建加载失败错误页(转义系统错误描述,避免注入)
+ // Build the load-failure error page (escape the system error description)
+ private fun errorPage(desc: String): String {
+ val safe = TextUtils.htmlEncode(desc)
+ return """
+
+
+ 加载失败
$safe
+ """.trimIndent() + } + // 进入沉浸式全屏:隐藏状态栏与导航栏,滑动可临时唤出 // Enter immersive fullscreen: hide status/navigation bars, swipe to reveal transiently private fun enterImmersive() { @@ -58,11 +134,24 @@ class MainActivity : Activity() { } } - // Back 键桌面态屏蔽(webAPP 打开时由 WebAppContainer 接管) - // Swallow Back key on desktop state (WebAppContainer takes over when a webAPP is open) + // Back 键:webAPP 打开时先回退,无历史则关闭激活标签;桌面态吞掉 + // Back key: when a webAPP is open, go back first, then close the active tab if + // no history; consume the event on desktop state @Suppress("DEPRECATION") override fun onBackPressed() { - // 桌面态:吞掉,不响应 - // Desktop state: consume the event, do not respond + val tabs = webAppContainer.tabs() + if (tabs.isNotEmpty()) { + val active = tabs.firstOrNull { it.active } + if (active != null) { + // goBack() 当前为占位 false,实际回退逻辑留 WebView 导航接入 + // goBack() is a placeholder returning false; real navigation lands later + if (!webAppContainer.goBack()) { + webAppContainer.close(active.id) + } + } + Log.d(TAG, "onBackPressed: webAPP back/close, remaining tabs=${webAppContainer.tabs().size}") + return + } + Log.d(TAG, "onBackPressed: desktop state, swallowed") } } diff --git a/app/src/main/java/top/yeij/hearth/webapp/OkHttpHttpClient.kt b/app/src/main/java/top/yeij/hearth/webapp/OkHttpHttpClient.kt new file mode 100644 index 0000000..11bd235 --- /dev/null +++ b/app/src/main/java/top/yeij/hearth/webapp/OkHttpHttpClient.kt @@ -0,0 +1,28 @@ +package top.yeij.hearth.webapp + +import android.util.Log +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.IOException + +// 基于 OkHttp 的 HttpClient 实现:GET 返回响应体字符串,失败/非 2xx 返回 null +// OkHttp-backed HttpClient: GET returns the response body string, null on failure or non-2xx +class OkHttpHttpClient( + private val client: OkHttpClient = OkHttpClient(), +) : HttpClient { + override fun get(url: String): String? { + return try { + val request = Request.Builder().url(url).get().build() + client.newCall(request).execute().use { resp -> + if (resp.isSuccessful) resp.body?.string() else null + } + } catch (e: IOException) { + Log.d(TAG, "get failed: $url -> ${e.message}") + null + } + } + + companion object { + private const val TAG = "HearthHttp" + } +}