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" + } +}