From ab2c158c2fa25f58fce9b4a8004ea08dadfebe3c Mon Sep 17 00:00:00 2001 From: haomingming Date: Tue, 10 Nov 2026 16:50:54 +0800 Subject: [PATCH] 4kvm --- .../com/tv/browser/DiagnosticsExporter.kt | 109 +++++++++++++++++- .../java/com/tv/browser/PlayerActivity.kt | 2 +- .../java/com/tv/browser/SettingsDialog.kt | 2 +- .../com/tv/browser/TVBrowserApplication.kt | 8 ++ 4 files changed, 113 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/tv/browser/DiagnosticsExporter.kt b/app/src/main/java/com/tv/browser/DiagnosticsExporter.kt index 864ae50..763d6a2 100644 --- a/app/src/main/java/com/tv/browser/DiagnosticsExporter.kt +++ b/app/src/main/java/com/tv/browser/DiagnosticsExporter.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.media.MediaCodecList import android.os.Build +import android.webkit.WebSettings import androidx.core.content.FileProvider import androidx.webkit.WebViewCompat import androidx.webkit.WebViewFeature @@ -57,8 +58,13 @@ object DiagnosticsExporter { val file = File(dir(context), "diagnostics_$stamp.txt") file.writeText(report, Charsets.UTF_8) Logx.i(TAG, "diagnostics saved: ${file.absolutePath} (${file.length()} bytes)") - val preview = report.lineSequence().take(12).joinToString("\n") - Result(file, preview, null) + // 摘要直接给出内核版本:这是电视端排查最常看的一项,不必打开文件 + val summary = buildString { + appendLine("WebView 内核: ${webViewSummary(context)}") + appendLine("系统: Android ${Build.VERSION.RELEASE} (API ${Build.VERSION.SDK_INT}) · ${Build.MANUFACTURER} ${Build.MODEL}") + append("运行日志: ${Logx.lineCount()} 行") + } + Result(file, summary, null) } catch (e: Throwable) { Logx.e(TAG, "write diagnostics failed", e) Result(null, "写入诊断日志失败", e.message ?: e.javaClass.simpleName) @@ -121,21 +127,101 @@ object DiagnosticsExporter { return sb.toString() } + /** WebView 内核探测结果:电视 ROM 经常让 getCurrentWebViewPackage() 返回空,所以要多路兜底 */ + data class WebViewInfo( + val packageName: String, + val version: String, + /** 版本号是怎么来的:包信息 / 系统 UA / 包管理器 / 未探测到 */ + val source: String, + /** 系统默认 UA(含真实 Chromium 版本号,即使包查询失败也能看出内核新旧) */ + val defaultUa: String, + /** 探测过程中出现的异常,导出后能看出「为什么取不到」 */ + val problem: String + ) { + fun oneLine(): String = "$version ($packageName) · 来源 $source" + } + + /** + * 探测 WebView 内核版本,三条路依次兜底: + * 1. [WebViewCompat.getCurrentWebViewPackage]:最准,但依赖系统把 WebView 作为独立包装着,部分电视返回 null + * 2. [WebSettings.getDefaultUserAgent]:不依赖 WebView 实例,UA 里带真实 `Chrome/xx.x.xxxx.xx` + * 3. 已知包名 + PackageManager:个别电视把 WebView 内嵌在系统应用里 + */ + fun detectWebView(context: Context): WebViewInfo { + val problems = StringBuilder() + var packageName: String? = null + var version: String? = null + var source = "未探测到" + + val pkg = runCatching { WebViewCompat.getCurrentWebViewPackage(context) } + .onFailure { problems.append("getCurrentWebViewPackage ${it.javaClass.simpleName}; ") } + .getOrNull() + if (pkg != null) { + packageName = pkg.packageName?.takeIf { it.isNotBlank() } + version = pkg.versionName?.takeIf { it.isNotBlank() } + if (version != null) source = "包信息" + } else { + problems.append("getCurrentWebViewPackage 返回 null; ") + } + + val defaultUa = runCatching { WebSettings.getDefaultUserAgent(context) } + .onFailure { problems.append("getDefaultUserAgent ${it.javaClass.simpleName}; ") } + .getOrNull() + .orEmpty() + if (version == null) { + CHROME_UA_REGEX.find(defaultUa)?.groupValues?.getOrNull(1)?.takeIf { it.isNotBlank() }?.let { + version = it + source = "系统 UA" + } + } + + if (packageName == null || version == null) { + for (candidate in WEBVIEW_PACKAGES) { + val info = packageInfoOrNull(context, candidate) ?: continue + if (packageName == null) packageName = candidate + if (version == null) { + version = info.versionName?.takeIf { it.isNotBlank() } + if (version != null) source = "包管理器" + } + if (version != null) break + } + } + + return WebViewInfo( + packageName = packageName ?: "未探测到", + version = version ?: "未探测到", + source = source, + defaultUa = defaultUa, + problem = problems.toString().trim().trimEnd(';').trim() + ) + } + /** WebView 内核版本与关键特性支持情况:电视端播放与页面异常的第一嫌疑点 */ private fun appendWebViewInfo(context: Context, sb: StringBuilder) { sb.appendLine("--- WebView 内核 ---") - val pkg = runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull() - sb.appendLine("包名: ${pkg?.packageName ?: "未知"}") - sb.appendLine("版本: ${pkg?.versionName ?: "未知"}") + val info = detectWebView(context) + sb.appendLine("版本: ${info.version}") + sb.appendLine("包名: ${info.packageName}") + sb.appendLine("来源: ${info.source}") + if (info.defaultUa.isNotBlank()) sb.appendLine("系统默认 UA: ${info.defaultUa}") sb.appendLine("特性支持:") FEATURES.forEach { (name, feature) -> val supported = runCatching { WebViewFeature.isFeatureSupported(feature) }.getOrDefault(false) sb.appendLine(" $name = ${if (supported) "YES" else "NO"}") } - sb.appendLine("说明: DOCUMENT_START_SCRIPT 需要 WebView 83+(为 NO 时嗅探钩子只能滞后注入)") + sb.appendLine("能力门槛参考: ??= 需 Chromium 85+ · aspect-ratio 需 88+ · inset 需 87+ · :has() 需 105+") + sb.appendLine("说明: DOCUMENT_START_SCRIPT 为 NO 时(<83)嗅探钩子只能滞后注入,可能漏掉首屏视频请求") + if (info.problem.isNotBlank()) sb.appendLine("探测异常: ${info.problem}") sb.appendLine() } + /** 供导出弹窗 / 错误报告用的内核摘要(不读文件、不枚举解码器,开销很小) */ + fun webViewSummary(context: Context): String = detectWebView(context).oneLine() + + /** 包信息查询包一层,避免个别 ROM 抛 NameNotFoundException 打断探测 */ + private fun packageInfoOrNull(context: Context, packageName: String): android.content.pm.PackageInfo? = + runCatching { context.packageManager.getPackageInfo(packageName, 0) }.getOrNull() + private fun appendNetworkInfo(context: Context, sb: StringBuilder) { sb.appendLine("--- 网络与系统状态 ---") val status = runCatching { NetworkChecker.getStatusMessage(context) }.getOrDefault("未知") @@ -223,6 +309,17 @@ object DiagnosticsExporter { private fun onOff(on: Boolean) = if (on) "开启" else "关闭" + /** 从系统默认 UA 里抠出真实 Chromium 版本号(WebView 包查询失败时的兜底来源) */ + private val CHROME_UA_REGEX = Regex("""Chrome/([\d.]+)""") + + /** 常见 WebView 包名:部分电视把 WebView 内嵌在系统应用里,包查询要挨个试 */ + private val WEBVIEW_PACKAGES = listOf( + "com.google.android.webview", + "com.android.webview", + "com.android.chrome", + "com.chrome.beta" + ) + /** * 关键 WebView 特性。启用状态直接影响嗅探链路: * - DOCUMENT_START_SCRIPT:能否在页面执行前注入 Hook(否则只能滞后注入,可能漏掉首屏视频请求) diff --git a/app/src/main/java/com/tv/browser/PlayerActivity.kt b/app/src/main/java/com/tv/browser/PlayerActivity.kt index 796a244..78ac10e 100644 --- a/app/src/main/java/com/tv/browser/PlayerActivity.kt +++ b/app/src/main/java/com/tv/browser/PlayerActivity.kt @@ -947,7 +947,7 @@ class PlayerActivity : AppCompatActivity() { val path = file.absolutePath androidx.appcompat.app.AlertDialog.Builder(this) .setTitle("✅ 诊断日志已导出") - .setMessage("${file.length() / 1024} KB\n$path\n\n可通过「手机遥控」页面下载,或复制到 U 盘") + .setMessage("${file.length() / 1024} KB\n$path\n\n${result.summary}\n\n可通过「手机遥控」页面下载,或复制到 U 盘") .setPositiveButton("分享") { _, _ -> if (!DiagnosticsExporter.share(this, file)) { Toast.makeText(this, "本机没有可用分享目标,请从上面的路径取文件", Toast.LENGTH_LONG).show() diff --git a/app/src/main/java/com/tv/browser/SettingsDialog.kt b/app/src/main/java/com/tv/browser/SettingsDialog.kt index 503e5dd..7379580 100644 --- a/app/src/main/java/com/tv/browser/SettingsDialog.kt +++ b/app/src/main/java/com/tv/browser/SettingsDialog.kt @@ -416,7 +416,7 @@ class SettingsDialog : DialogFragment() { "1. 手机遥控页(手机与电视同一 WiFi)点「下载电视日志」\n" + "2. 电视文件管理器进入 Android/data/${ctx.packageName}/files/${file.parentFile?.name}/\n" + "3. 点下方「分享」发到可用目标\n\n" + - "--- 预览 ---\n${result.summary}" + "--- 摘要 ---\n${result.summary}" ) .setPositiveButton("分享") { _, _ -> if (!DiagnosticsExporter.share(ctx, file)) { diff --git a/app/src/main/java/com/tv/browser/TVBrowserApplication.kt b/app/src/main/java/com/tv/browser/TVBrowserApplication.kt index 9d749cd..cce9a19 100644 --- a/app/src/main/java/com/tv/browser/TVBrowserApplication.kt +++ b/app/src/main/java/com/tv/browser/TVBrowserApplication.kt @@ -38,6 +38,14 @@ class TVBrowserApplication : Application() { try { dirs.forEach { pruneCrashLogs(it) } } catch (_: Throwable) { } + // 启动即记录内核与环境:导出的运行日志尾部也能看到,不必依赖「WebView 内核」小节 + try { + Logx.i( + "TVB.Env", + "app start webview=${DiagnosticsExporter.webViewSummary(this)} " + + "android=${Build.VERSION.RELEASE}(${Build.VERSION.SDK_INT}) ${Build.MANUFACTURER} ${Build.MODEL}" + ) + } catch (_: Throwable) { } }.start() }