4kvm
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user