This commit is contained in:
haomingming
2026-11-10 16:50:54 +08:00
parent d3ea2e35d7
commit ab2c158c2f
4 changed files with 113 additions and 8 deletions
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.MediaCodecList import android.media.MediaCodecList
import android.os.Build import android.os.Build
import android.webkit.WebSettings
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.webkit.WebViewCompat import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature import androidx.webkit.WebViewFeature
@@ -57,8 +58,13 @@ object DiagnosticsExporter {
val file = File(dir(context), "diagnostics_$stamp.txt") val file = File(dir(context), "diagnostics_$stamp.txt")
file.writeText(report, Charsets.UTF_8) file.writeText(report, Charsets.UTF_8)
Logx.i(TAG, "diagnostics saved: ${file.absolutePath} (${file.length()} bytes)") 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) { } catch (e: Throwable) {
Logx.e(TAG, "write diagnostics failed", e) Logx.e(TAG, "write diagnostics failed", e)
Result(null, "写入诊断日志失败", e.message ?: e.javaClass.simpleName) Result(null, "写入诊断日志失败", e.message ?: e.javaClass.simpleName)
@@ -121,21 +127,101 @@ object DiagnosticsExporter {
return sb.toString() 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 内核版本与关键特性支持情况:电视端播放与页面异常的第一嫌疑点 */ /** WebView 内核版本与关键特性支持情况:电视端播放与页面异常的第一嫌疑点 */
private fun appendWebViewInfo(context: Context, sb: StringBuilder) { private fun appendWebViewInfo(context: Context, sb: StringBuilder) {
sb.appendLine("--- WebView 内核 ---") sb.appendLine("--- WebView 内核 ---")
val pkg = runCatching { WebViewCompat.getCurrentWebViewPackage(context) }.getOrNull() val info = detectWebView(context)
sb.appendLine("包名: ${pkg?.packageName ?: "未知"}") sb.appendLine("版本: ${info.version}")
sb.appendLine("版本: ${pkg?.versionName ?: "未知"}") sb.appendLine("包名: ${info.packageName}")
sb.appendLine("来源: ${info.source}")
if (info.defaultUa.isNotBlank()) sb.appendLine("系统默认 UA: ${info.defaultUa}")
sb.appendLine("特性支持:") sb.appendLine("特性支持:")
FEATURES.forEach { (name, feature) -> FEATURES.forEach { (name, feature) ->
val supported = runCatching { WebViewFeature.isFeatureSupported(feature) }.getOrDefault(false) val supported = runCatching { WebViewFeature.isFeatureSupported(feature) }.getOrDefault(false)
sb.appendLine(" $name = ${if (supported) "YES" else "NO"}") 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() 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) { private fun appendNetworkInfo(context: Context, sb: StringBuilder) {
sb.appendLine("--- 网络与系统状态 ---") sb.appendLine("--- 网络与系统状态 ---")
val status = runCatching { NetworkChecker.getStatusMessage(context) }.getOrDefault("未知") val status = runCatching { NetworkChecker.getStatusMessage(context) }.getOrDefault("未知")
@@ -223,6 +309,17 @@ object DiagnosticsExporter {
private fun onOff(on: Boolean) = if (on) "开启" else "关闭" 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 特性。启用状态直接影响嗅探链路: * 关键 WebView 特性。启用状态直接影响嗅探链路:
* - DOCUMENT_START_SCRIPT:能否在页面执行前注入 Hook(否则只能滞后注入,可能漏掉首屏视频请求) * - DOCUMENT_START_SCRIPT:能否在页面执行前注入 Hook(否则只能滞后注入,可能漏掉首屏视频请求)
@@ -947,7 +947,7 @@ class PlayerActivity : AppCompatActivity() {
val path = file.absolutePath val path = file.absolutePath
androidx.appcompat.app.AlertDialog.Builder(this) androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("✅ 诊断日志已导出") .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("分享") { _, _ -> .setPositiveButton("分享") { _, _ ->
if (!DiagnosticsExporter.share(this, file)) { if (!DiagnosticsExporter.share(this, file)) {
Toast.makeText(this, "本机没有可用分享目标,请从上面的路径取文件", Toast.LENGTH_LONG).show() Toast.makeText(this, "本机没有可用分享目标,请从上面的路径取文件", Toast.LENGTH_LONG).show()
@@ -416,7 +416,7 @@ class SettingsDialog : DialogFragment() {
"1. 手机遥控页(手机与电视同一 WiFi)点「下载电视日志」\n" + "1. 手机遥控页(手机与电视同一 WiFi)点「下载电视日志」\n" +
"2. 电视文件管理器进入 Android/data/${ctx.packageName}/files/${file.parentFile?.name}/\n" + "2. 电视文件管理器进入 Android/data/${ctx.packageName}/files/${file.parentFile?.name}/\n" +
"3. 点下方「分享」发到可用目标\n\n" + "3. 点下方「分享」发到可用目标\n\n" +
"--- 预览 ---\n${result.summary}" "--- 摘要 ---\n${result.summary}"
) )
.setPositiveButton("分享") { _, _ -> .setPositiveButton("分享") { _, _ ->
if (!DiagnosticsExporter.share(ctx, file)) { if (!DiagnosticsExporter.share(ctx, file)) {
@@ -38,6 +38,14 @@ class TVBrowserApplication : Application() {
try { try {
dirs.forEach { pruneCrashLogs(it) } dirs.forEach { pruneCrashLogs(it) }
} catch (_: Throwable) { } } 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() }.start()
} }