feat: per-app UA config, fix demo file path and back-loop
This commit is contained in:
@@ -267,13 +267,16 @@ class MainActivity : Activity() {
|
|||||||
private val sidebarWidthPx by lazy {
|
private val sidebarWidthPx by lazy {
|
||||||
(SIDEBAR_WIDTH_DP * resources.displayMetrics.density).toInt()
|
(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<String>()
|
||||||
|
|
||||||
override fun openWebView(id: String, url: String) {
|
override fun openWebView(id: String, url: String, ua: String?) {
|
||||||
if (contentWebViews.containsKey(id)) {
|
if (contentWebViews.containsKey(id)) {
|
||||||
switchWebView(id)
|
switchWebView(id)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val webView = WebViewManager(this@MainActivity).createContentWebView()
|
val webView = WebViewManager(this@MainActivity).createContentWebView(ua)
|
||||||
webView.webViewClient = object : WebViewClient() {
|
webView.webViewClient = object : WebViewClient() {
|
||||||
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
@Suppress("DEPRECATION", "OVERRIDE_DEPRECATION")
|
||||||
override fun onReceivedError(
|
override fun onReceivedError(
|
||||||
@@ -283,6 +286,7 @@ class MainActivity : Activity() {
|
|||||||
failingUrl: String?,
|
failingUrl: String?,
|
||||||
) {
|
) {
|
||||||
Log.d(TAG, "content onReceivedError: code=$code desc=$description url=$failingUrl")
|
Log.d(TAG, "content onReceivedError: code=$code desc=$description url=$failingUrl")
|
||||||
|
failedWebViews.add(id)
|
||||||
view?.loadDataWithBaseURL(null, errorPage(description ?: "未知错误"), "text/html", "utf-8", null)
|
view?.loadDataWithBaseURL(null, errorPage(description ?: "未知错误"), "text/html", "utf-8", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -311,6 +315,7 @@ class MainActivity : Activity() {
|
|||||||
|
|
||||||
override fun closeWebView(id: String) {
|
override fun closeWebView(id: String) {
|
||||||
val webView = contentWebViews.remove(id) ?: return
|
val webView = contentWebViews.remove(id) ?: return
|
||||||
|
failedWebViews.remove(id)
|
||||||
root.removeView(webView)
|
root.removeView(webView)
|
||||||
webView.stopLoading()
|
webView.stopLoading()
|
||||||
webView.destroy()
|
webView.destroy()
|
||||||
@@ -333,8 +338,12 @@ class MainActivity : Activity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun goBack(): Boolean {
|
override fun goBack(): Boolean {
|
||||||
val webView = currentVisibleId?.let { contentWebViews[it] }
|
val id = currentVisibleId ?: return false
|
||||||
return if (webView != null && webView.canGoBack()) {
|
// 加载失败的 webapp:Back 直接关闭(返回 false 让 onBackPressed 关闭标签),
|
||||||
|
// 避免 goBack 回退到失败的 file:// 页面反复加载
|
||||||
|
if (failedWebViews.contains(id)) return false
|
||||||
|
val webView = contentWebViews[id] ?: return false
|
||||||
|
return if (webView.canGoBack()) {
|
||||||
webView.goBack()
|
webView.goBack()
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ data class WebApp(
|
|||||||
val url: String,
|
val url: String,
|
||||||
val offline: String? = null,
|
val offline: String? = null,
|
||||||
val offlineVersion: String? = null,
|
val offlineVersion: String? = null,
|
||||||
|
val ua: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
// 网络抽象接口:Task 10 的 CardRepository 也会复用
|
// 网络抽象接口:Task 10 的 CardRepository 也会复用
|
||||||
@@ -106,12 +107,17 @@ class WebAppRepository(
|
|||||||
val offlineMap = map["offline"] as? Map<*, *>
|
val offlineMap = map["offline"] as? Map<*, *>
|
||||||
val offline = offlineMap?.get("package") as? String
|
val offline = offlineMap?.get("package") as? String
|
||||||
val offlineVersion = offlineMap?.get("version") 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(
|
WebApp(
|
||||||
id, name,
|
id, name,
|
||||||
resolvePath(icon),
|
resolvePath(icon),
|
||||||
resolvePath(url),
|
resolvePath(url),
|
||||||
offline?.let { resolvePath(it) },
|
offline?.let { resolvePath(it) },
|
||||||
offlineVersion,
|
offlineVersion,
|
||||||
|
ua,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,9 +26,21 @@ class WebAppStorage(private val context: Context) {
|
|||||||
File(appDir(id), "index.html").exists()
|
File(appDir(id), "index.html").exists()
|
||||||
|
|
||||||
// 本地离线包入口 URL(file:// 绝对路径),无则 null
|
// 本地离线包入口 URL(file:// 绝对路径),无则 null
|
||||||
// Local offline-package entry URL (file:// absolute path), null when absent
|
// canonicalPath 解析 /data/user/0 -> /data/data 符号链接,避免 WebView 的
|
||||||
fun localIndexUrl(id: String): String? =
|
// ERR_ACCESS_DENIED
|
||||||
if (hasLocalPackage(id)) "file://" + File(appDir(id), "index.html").absolutePath else null
|
// 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
|
// 本地离线包版本号(解压目录里的 .version 文件),无则 null
|
||||||
// Local offline-package version (from the .version file), null when absent
|
// Local offline-package version (from the .version file), null when absent
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class JsBridge(
|
|||||||
// 1. 本地离线包优先(服务端删除后仍可用)
|
// 1. 本地离线包优先(服务端删除后仍可用)
|
||||||
val localUrl = st?.localIndexUrl(id)
|
val localUrl = st?.localIndexUrl(id)
|
||||||
if (localUrl != null) {
|
if (localUrl != null) {
|
||||||
openWebAppAt(container, host, id, localUrl, app.name)
|
openWebAppAt(container, host, id, localUrl, app.name, app.ua)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 2. 有离线包 → 后台下载解压(推送进度),完成后加载本地;失败回退远程
|
// 2. 有离线包 → 后台下载解压(推送进度),完成后加载本地;失败回退远程
|
||||||
@@ -155,24 +155,24 @@ class JsBridge(
|
|||||||
val fullUrl = resolveUrl(pkg)
|
val fullUrl = resolveUrl(pkg)
|
||||||
st.downloadAndExtract(id, fullUrl, app.offlineVersion ?: "") { p -> pushWebappProgress(id, p) }
|
st.downloadAndExtract(id, fullUrl, app.offlineVersion ?: "") { p -> pushWebappProgress(id, p) }
|
||||||
val url = st.localIndexUrl(id) ?: app.url
|
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)
|
pushWebappProgress(id, 100)
|
||||||
}.start()
|
}.start()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 3. 无离线包 → 远程加载(WebView 自带加载进度)
|
// 3. 无离线包 → 远程加载(WebView 自带加载进度)
|
||||||
openWebAppAt(container, host, id, app.url, app.name)
|
openWebAppAt(container, host, id, app.url, app.name, app.ua)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在主线程打开指定 url 的 webAPP 标签
|
// 在主线程打开指定 url 的 webAPP 标签
|
||||||
// Open a webapp tab at the given url on the main thread
|
// 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 {
|
onMain {
|
||||||
container.open(id, url, name)
|
container.open(id, url, name)
|
||||||
host.openWebView(id, url)
|
host.openWebView(id, url, ua)
|
||||||
host.syncTabs(container.tabs())
|
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)
|
// 手动导入离线包(由 MainActivity 触发 SAF 选择 zip)
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import top.yeij.hearth.webapp.Tab
|
|||||||
// multi-WebView lifecycle, visibility, and navigation, and syncing tab state
|
// multi-WebView lifecycle, visibility, and navigation, and syncing tab state
|
||||||
// back to the desktop H5 topbar
|
// back to the desktop H5 topbar
|
||||||
interface WebAppHost {
|
interface WebAppHost {
|
||||||
// 创建内容 WebView 加载 url 并显示(已存在则切换到该标签)
|
// 创建内容 WebView 加载 url 并显示(已存在则切换到该标签);ua 为清单声明的 UA
|
||||||
// Create a content WebView loading url and show it (switch if already open)
|
// Create a content WebView loading url and show it (switch if already open);
|
||||||
fun openWebView(id: String, url: String)
|
// ua is the UA declared in the manifest
|
||||||
|
fun openWebView(id: String, url: String, ua: String?)
|
||||||
|
|
||||||
// 销毁对应内容 WebView
|
// 销毁对应内容 WebView
|
||||||
// Destroy the corresponding content WebView
|
// Destroy the corresponding content WebView
|
||||||
|
|||||||
@@ -31,10 +31,18 @@ class WebViewManager(private val activity: Activity) {
|
|||||||
// (lifecycle is managed by the host); transparent background so a webapp without
|
// (lifecycle is managed by the host); transparent background so a webapp without
|
||||||
// a declared background shows the wallpaper instead of a default white
|
// a declared background shows the wallpaper instead of a default white
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
fun createContentWebView(): WebView {
|
fun createContentWebView(ua: String?): WebView {
|
||||||
val webView = WebView(activity)
|
val webView = WebView(activity)
|
||||||
webView.setBackgroundColor(Color.TRANSPARENT)
|
webView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
configure(webView)
|
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
|
return webView
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,4 +58,15 @@ class WebViewManager(private val activity: Activity) {
|
|||||||
domStorageEnabled = true
|
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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user