diff --git a/app/src/main/java/top/yeij/hearth/MainActivity.kt b/app/src/main/java/top/yeij/hearth/MainActivity.kt index 9497918..1b9ffad 100644 --- a/app/src/main/java/top/yeij/hearth/MainActivity.kt +++ b/app/src/main/java/top/yeij/hearth/MainActivity.kt @@ -267,13 +267,16 @@ class MainActivity : Activity() { private val sidebarWidthPx by lazy { (SIDEBAR_WIDTH_DP * resources.displayMetrics.density).toInt() } + // 加载失败的 webapp id:Back 键时直接关闭而非 goBack 循环回退 + // Failed webapp ids: on Back, close directly instead of looping goBack + private val failedWebViews = mutableSetOf() - override fun openWebView(id: String, url: String) { + override fun openWebView(id: String, url: String, ua: String?) { if (contentWebViews.containsKey(id)) { switchWebView(id) return } - val webView = WebViewManager(this@MainActivity).createContentWebView() + val webView = WebViewManager(this@MainActivity).createContentWebView(ua) webView.webViewClient = object : WebViewClient() { @Suppress("DEPRECATION", "OVERRIDE_DEPRECATION") override fun onReceivedError( @@ -283,6 +286,7 @@ class MainActivity : Activity() { failingUrl: String?, ) { Log.d(TAG, "content onReceivedError: code=$code desc=$description url=$failingUrl") + failedWebViews.add(id) view?.loadDataWithBaseURL(null, errorPage(description ?: "未知错误"), "text/html", "utf-8", null) } } @@ -311,6 +315,7 @@ class MainActivity : Activity() { override fun closeWebView(id: String) { val webView = contentWebViews.remove(id) ?: return + failedWebViews.remove(id) root.removeView(webView) webView.stopLoading() webView.destroy() @@ -333,8 +338,12 @@ class MainActivity : Activity() { } override fun goBack(): Boolean { - val webView = currentVisibleId?.let { contentWebViews[it] } - return if (webView != null && webView.canGoBack()) { + val id = currentVisibleId ?: return false + // 加载失败的 webapp:Back 直接关闭(返回 false 让 onBackPressed 关闭标签), + // 避免 goBack 回退到失败的 file:// 页面反复加载 + if (failedWebViews.contains(id)) return false + val webView = contentWebViews[id] ?: return false + return if (webView.canGoBack()) { webView.goBack() true } else { diff --git a/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt index dc012e3..748e486 100644 --- a/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt +++ b/app/src/main/java/top/yeij/hearth/webapp/WebAppRepository.kt @@ -16,6 +16,7 @@ data class WebApp( val url: String, val offline: String? = null, val offlineVersion: String? = null, + val ua: String? = null, ) // 网络抽象接口:Task 10 的 CardRepository 也会复用 @@ -106,12 +107,17 @@ class WebAppRepository( val offlineMap = map["offline"] as? Map<*, *> val offline = offlineMap?.get("package") as? String val offlineVersion = offlineMap?.get("version") as? String + // ua 可选:desktop/pc 用 PC UA,其他视为自定义 UA 字符串,不指定默认平板 UA + // ua optional: desktop/pc uses a desktop UA, others are custom UA strings, + // absent means the default tablet UA + val ua = map["ua"] as? String WebApp( id, name, resolvePath(icon), resolvePath(url), offline?.let { resolvePath(it) }, offlineVersion, + ua, ) } } diff --git a/app/src/main/java/top/yeij/hearth/webapp/WebAppStorage.kt b/app/src/main/java/top/yeij/hearth/webapp/WebAppStorage.kt index 82e9c7f..f50c5fa 100644 --- a/app/src/main/java/top/yeij/hearth/webapp/WebAppStorage.kt +++ b/app/src/main/java/top/yeij/hearth/webapp/WebAppStorage.kt @@ -26,9 +26,21 @@ class WebAppStorage(private val context: Context) { File(appDir(id), "index.html").exists() // 本地离线包入口 URL(file:// 绝对路径),无则 null - // Local offline-package entry URL (file:// absolute path), null when absent - fun localIndexUrl(id: String): String? = - if (hasLocalPackage(id)) "file://" + File(appDir(id), "index.html").absolutePath else null + // canonicalPath 解析 /data/user/0 -> /data/data 符号链接,避免 WebView 的 + // ERR_ACCESS_DENIED + // Local offline-package entry URL (file:// absolute path), null when absent. + // canonicalPath resolves the /data/user/0 -> /data/data symlink, avoiding the + // WebView ERR_ACCESS_DENIED + fun localIndexUrl(id: String): String? { + if (!hasLocalPackage(id)) return null + val f = File(appDir(id), "index.html") + val path = try { + f.canonicalPath + } catch (e: Exception) { + f.absolutePath + } + return "file://" + path + } // 本地离线包版本号(解压目录里的 .version 文件),无则 null // Local offline-package version (from the .version file), null when absent 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 b758a1d..4ed08e3 100644 --- a/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt +++ b/app/src/main/java/top/yeij/hearth/webview/JsBridge.kt @@ -144,7 +144,7 @@ class JsBridge( // 1. 本地离线包优先(服务端删除后仍可用) val localUrl = st?.localIndexUrl(id) if (localUrl != null) { - openWebAppAt(container, host, id, localUrl, app.name) + openWebAppAt(container, host, id, localUrl, app.name, app.ua) return } // 2. 有离线包 → 后台下载解压(推送进度),完成后加载本地;失败回退远程 @@ -155,24 +155,24 @@ class JsBridge( val fullUrl = resolveUrl(pkg) st.downloadAndExtract(id, fullUrl, app.offlineVersion ?: "") { p -> pushWebappProgress(id, p) } val url = st.localIndexUrl(id) ?: app.url - openWebAppAt(container, host, id, url, app.name) + openWebAppAt(container, host, id, url, app.name, app.ua) pushWebappProgress(id, 100) }.start() return } // 3. 无离线包 → 远程加载(WebView 自带加载进度) - openWebAppAt(container, host, id, app.url, app.name) + openWebAppAt(container, host, id, app.url, app.name, app.ua) } // 在主线程打开指定 url 的 webAPP 标签 // Open a webapp tab at the given url on the main thread - private fun openWebAppAt(container: WebAppContainer, host: WebAppHost, id: String, url: String, name: String) { + private fun openWebAppAt(container: WebAppContainer, host: WebAppHost, id: String, url: String, name: String, ua: String?) { onMain { container.open(id, url, name) - host.openWebView(id, url) + host.openWebView(id, url, ua) host.syncTabs(container.tabs()) } - Log.d("HearthBridge", "openWebApp: id=$id url=$url") + Log.d("HearthBridge", "openWebApp: id=$id url=$url ua=$ua") } // 手动导入离线包(由 MainActivity 触发 SAF 选择 zip) diff --git a/app/src/main/java/top/yeij/hearth/webview/WebAppHost.kt b/app/src/main/java/top/yeij/hearth/webview/WebAppHost.kt index 43f8472..ec8cf31 100644 --- a/app/src/main/java/top/yeij/hearth/webview/WebAppHost.kt +++ b/app/src/main/java/top/yeij/hearth/webview/WebAppHost.kt @@ -8,9 +8,10 @@ import top.yeij.hearth.webapp.Tab // multi-WebView lifecycle, visibility, and navigation, and syncing tab state // back to the desktop H5 topbar interface WebAppHost { - // 创建内容 WebView 加载 url 并显示(已存在则切换到该标签) - // Create a content WebView loading url and show it (switch if already open) - fun openWebView(id: String, url: String) + // 创建内容 WebView 加载 url 并显示(已存在则切换到该标签);ua 为清单声明的 UA + // Create a content WebView loading url and show it (switch if already open); + // ua is the UA declared in the manifest + fun openWebView(id: String, url: String, ua: String?) // 销毁对应内容 WebView // Destroy the corresponding content WebView diff --git a/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt b/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt index 0ee52c5..50eda8a 100644 --- a/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt +++ b/app/src/main/java/top/yeij/hearth/webview/WebViewManager.kt @@ -31,10 +31,18 @@ class WebViewManager(private val activity: Activity) { // (lifecycle is managed by the host); transparent background so a webapp without // a declared background shows the wallpaper instead of a default white @SuppressLint("SetJavaScriptEnabled") - fun createContentWebView(): WebView { + fun createContentWebView(ua: String?): WebView { val webView = WebView(activity) webView.setBackgroundColor(Color.TRANSPARENT) configure(webView) + // UA 解析:desktop/pc → PC UA;自定义字符串 → 直接用;null → 默认平板 UA + // UA resolution: desktop/pc -> desktop UA; custom string -> used as-is; + // null -> default tablet UA + webView.settings.userAgentString = when { + ua == null -> UA_TABLET + ua == "desktop" || ua == "pc" -> UA_DESKTOP + else -> ua + } return webView } @@ -50,4 +58,15 @@ class WebViewManager(private val activity: Activity) { domStorageEnabled = true } } + + companion object { + // PC 版 UA(desktop/pc) + private const val UA_DESKTOP = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + + "(KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + // 安卓平板 UA(默认) + private const val UA_TABLET = + "Mozilla/5.0 (Linux; Android 13; SM-X700 Build/TP1A.220624.014) " + + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + } }