fix: docx MIME 检测 + Fingerprint 协程帧稳定性
sdk-webview: 选文件时 DISPLAY_NAME 在 vivo content://media/external/file/ URI 上返回 null,导致扩展名推断失效;ContentResolver.getType() 对 .docx 返回 application/msword(错误)→ allowed 检查拒绝文件。 修复:DISPLAY_NAME 为 null 时回退查 MediaStore.DATA 提取文件名; 优先使用扩展名推断的 MIME 而非 ContentResolver 的返回值, 避免厂商 ROM 返回错误 MIME 的干扰。copyUriToWebViewCache 同步修复。 sdk-bugcollect: Fingerprint.compute 取 top3 stack 帧,Kotlin 协程 dispatcher 的 第 2、3 帧在每次调度时不固定,相同逻辑错误产生不同指纹 → 多条 issue。 修改为 top1(只取抛出点帧),保证同一位置抛出的异常总被归为一条。 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
ab0dc90909
当前提交
3d62a6ed72
@ -2,10 +2,13 @@ package com.xuqm.sdk.bugcollect
|
||||
|
||||
object Fingerprint {
|
||||
// exceptionType = exception class name (e.g. "NullPointerException"), NOT the level string
|
||||
// Only the innermost "at" frame (throw site) is used for grouping.
|
||||
// Kotlin coroutine dispatcher frames (frame 2+) vary across runs of the same logical error,
|
||||
// which caused identical errors to create separate issues.
|
||||
fun compute(exceptionType: String, message: String, stack: String): String {
|
||||
val top3 = stack.lines().filter { it.trim().startsWith("at ") }.take(3).joinToString("|")
|
||||
val top1 = stack.lines().filter { it.trim().startsWith("at ") }.take(1).joinToString("|")
|
||||
val normalized = message.replace(Regex("\\b\\d{4,}\\b"), "N")
|
||||
return sha256("$exceptionType:$normalized:$top3")
|
||||
return sha256("$exceptionType:$normalized:$top1")
|
||||
}
|
||||
|
||||
private fun sha256(s: String): String =
|
||||
|
||||
@ -272,12 +272,21 @@ internal fun storagePermissionsGranted(context: Context, acceptMimes: Array<Stri
|
||||
*/
|
||||
internal fun copyUriToWebViewCache(context: Context, uri: Uri, mimeType: String?): Uri? {
|
||||
return runCatching {
|
||||
val displayName = context.contentResolver.query(
|
||||
// Query display name; fall back to file path (DATA column) on vivo / OPPO devices where
|
||||
// DISPLAY_NAME is not populated for content://media/external/file/... URIs.
|
||||
val displayName: String? = context.contentResolver.query(
|
||||
uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null
|
||||
)?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
||||
?: runCatching {
|
||||
context.contentResolver.query(
|
||||
uri, arrayOf(android.provider.MediaStore.MediaColumns.DATA), null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) cursor.getString(0)?.substringAfterLast('/') else null
|
||||
}
|
||||
}.getOrNull()
|
||||
|
||||
val ext = mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
|
||||
?: displayName?.substringAfterLast('.', "").takeIf { !it.isNullOrBlank() }
|
||||
val ext = displayName?.substringAfterLast('.', "")?.takeIf { it.isNotBlank() }
|
||||
?: mimeType?.let { MimeTypeMap.getSingleton().getExtensionFromMimeType(it) }
|
||||
?: "bin"
|
||||
val base = displayName?.substringBeforeLast('.')
|
||||
?.replace(Regex("[^a-zA-Z0-9_\\-]"), "_")
|
||||
@ -464,22 +473,27 @@ fun XWebViewView(
|
||||
return@rememberLauncherForActivityResult
|
||||
}
|
||||
val acceptedMimes = pendingAcceptMimes.value
|
||||
// ContentResolver returns "application/octet-stream" (or null) for most Office files
|
||||
// from file-system / Downloads providers. Fall back to extension-based inference so
|
||||
// .docx / .doc / .xls etc. are not incorrectly rejected.
|
||||
val rawMime = context.contentResolver.getType(uri)
|
||||
val displayName = context.contentResolver.query(
|
||||
// Query display name; fall back to MediaStore.DATA file path on devices (e.g. vivo) where
|
||||
// DISPLAY_NAME is null for content://media/external/file/... URIs.
|
||||
val displayName: String? = context.contentResolver.query(
|
||||
uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null
|
||||
)?.use { cursor -> if (cursor.moveToFirst()) cursor.getString(0) else null }
|
||||
?: runCatching {
|
||||
context.contentResolver.query(
|
||||
uri, arrayOf(android.provider.MediaStore.MediaColumns.DATA), null, null, null
|
||||
)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) cursor.getString(0)?.substringAfterLast('/') else null
|
||||
}
|
||||
}.getOrNull()
|
||||
android.util.Log.d(XWV_FILE, " displayName=$displayName rawMime=$rawMime acceptedMimes=$acceptedMimes")
|
||||
val actualMime: String? = if (rawMime == null || rawMime == "application/octet-stream") {
|
||||
val ext = displayName?.substringAfterLast('.', "")
|
||||
?.lowercase(Locale.ROOT)
|
||||
?.takeIf { it.isNotEmpty() }
|
||||
val ext = displayName?.substringAfterLast('.', "")?.lowercase(Locale.ROOT)?.takeIf { it.isNotEmpty() }
|
||||
val extMime = ext?.let { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) }
|
||||
android.util.Log.d(XWV_FILE, " rawMime generic → ext=$ext extMime=$extMime")
|
||||
extMime ?: rawMime
|
||||
} else rawMime
|
||||
android.util.Log.d(XWV_FILE, " ext=$ext extMime=$extMime")
|
||||
// Prefer extension-based MIME type: ContentResolver can return wrong types for Office files
|
||||
// on some devices (e.g. vivo reports application/msword for .docx files).
|
||||
// Fall back to rawMime only when we have no extension information.
|
||||
val actualMime: String? = extMime ?: rawMime
|
||||
android.util.Log.d(XWV_FILE, " actualMime=$actualMime")
|
||||
val allowed = acceptedMimes.isNullOrEmpty() ||
|
||||
acceptedMimes.any { accepted ->
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户