feat: wire MainActivity with repositories, error page, and back handling
This commit is contained in:
@@ -3,44 +3,90 @@ package top.yeij.hearth
|
|||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.TextUtils
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.view.WindowInsetsController
|
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.AndroidAppSource
|
||||||
import top.yeij.hearth.app.AppRepository
|
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.JsBridge
|
||||||
import top.yeij.hearth.webview.WebViewManager
|
import top.yeij.hearth.webview.WebViewManager
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
// HOME 桌面 activity,后续 Task 挂载 WebView 与 JsBridge
|
// HOME 桌面 activity,接线各能力层:Repository、媒体监听、WebView 错误页与 Back 键
|
||||||
// HOME launcher activity; later tasks mount WebView and JsBridge
|
// HOME launcher activity, wiring all capability layers: repositories, media listener,
|
||||||
|
// WebView error page, and Back key handling
|
||||||
class MainActivity : Activity() {
|
class MainActivity : Activity() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "HearthMainActivity"
|
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?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(TAG, "onCreate: enter immersive mode")
|
Log.d(TAG, "onCreate: enter immersive mode")
|
||||||
enterImmersive()
|
enterImmersive()
|
||||||
setupWebView()
|
setupWebView()
|
||||||
|
checkNotificationAccess()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建 JsBridge 并挂载 WebView 桌面层
|
override fun onDestroy() {
|
||||||
// Create JsBridge and mount the WebView launcher layer
|
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() {
|
private fun setupWebView() {
|
||||||
val dm = resources.displayMetrics
|
val dm = resources.displayMetrics
|
||||||
val darkMode =
|
val darkMode =
|
||||||
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
|
||||||
Configuration.UI_MODE_NIGHT_YES
|
Configuration.UI_MODE_NIGHT_YES
|
||||||
|
val cache = CacheManager(File(filesDir, "cache"))
|
||||||
|
val http = OkHttpHttpClient()
|
||||||
val bridge = JsBridge(
|
val bridge = JsBridge(
|
||||||
deviceWidthPx = dm.widthPixels,
|
deviceWidthPx = dm.widthPixels,
|
||||||
deviceHeightPx = dm.heightPixels,
|
deviceHeightPx = dm.heightPixels,
|
||||||
density = dm.density,
|
density = dm.density,
|
||||||
darkMode = darkMode,
|
darkMode = darkMode,
|
||||||
appRepository = AppRepository(AndroidAppSource(this)),
|
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(
|
Log.d(
|
||||||
TAG,
|
TAG,
|
||||||
"setupWebView: attached WebView ${dm.widthPixels}x${dm.heightPixels}" +
|
"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 """
|
||||||
|
<!DOCTYPE html><html lang="zh"><head><meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
body{font-family:sans-serif;display:flex;flex-direction:column;align-items:center;
|
||||||
|
justify-content:center;height:100vh;margin:0;background:transparent;color:#999}
|
||||||
|
h2{font-weight:400;color:#ccc}
|
||||||
|
p{font-size:14px;max-width:80vw;text-align:center;word-break:break-all}
|
||||||
|
</style></head><body><h2>加载失败</h2><p>$safe</p></body></html>
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
|
||||||
// 进入沉浸式全屏:隐藏状态栏与导航栏,滑动可临时唤出
|
// 进入沉浸式全屏:隐藏状态栏与导航栏,滑动可临时唤出
|
||||||
// Enter immersive fullscreen: hide status/navigation bars, swipe to reveal transiently
|
// Enter immersive fullscreen: hide status/navigation bars, swipe to reveal transiently
|
||||||
private fun enterImmersive() {
|
private fun enterImmersive() {
|
||||||
@@ -58,11 +134,24 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Back 键桌面态屏蔽(webAPP 打开时由 WebAppContainer 接管)
|
// Back 键:webAPP 打开时先回退,无历史则关闭激活标签;桌面态吞掉
|
||||||
// Swallow Back key on desktop state (WebAppContainer takes over when a webAPP is open)
|
// 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")
|
@Suppress("DEPRECATION")
|
||||||
override fun onBackPressed() {
|
override fun onBackPressed() {
|
||||||
// 桌面态:吞掉,不响应
|
val tabs = webAppContainer.tabs()
|
||||||
// Desktop state: consume the event, do not respond
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user