From 3d62a6ed720c6c3783fb794dd57646f4e6ba65cc Mon Sep 17 00:00:00 2001 From: XuqmGroup Date: Tue, 23 Jun 2026 15:19:51 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20docx=20MIME=20=E6=A3=80=E6=B5=8B=20+=20F?= =?UTF-8?q?ingerprint=20=E5=8D=8F=E7=A8=8B=E5=B8=A7=E7=A8=B3=E5=AE=9A?= =?UTF-8?q?=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../com/xuqm/sdk/bugcollect/Fingerprint.kt | 7 ++- .../java/com/xuqm/sdk/webview/XWebViewView.kt | 44 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt index 2e61475..19b8deb 100644 --- a/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt +++ b/sdk-bugcollect/src/main/java/com/xuqm/sdk/bugcollect/Fingerprint.kt @@ -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 = diff --git a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt index 110dad2..9ff1676 100644 --- a/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt +++ b/sdk-webview/src/main/java/com/xuqm/sdk/webview/XWebViewView.kt @@ -272,12 +272,21 @@ internal fun storagePermissionsGranted(context: Context, acceptMimes: Array 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 extMime = ext?.let { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) } - android.util.Log.d(XWV_FILE, " rawMime generic → ext=$ext extMime=$extMime") - extMime ?: rawMime - } else rawMime + val ext = displayName?.substringAfterLast('.', "")?.lowercase(Locale.ROOT)?.takeIf { it.isNotEmpty() } + val extMime = ext?.let { MimeTypeMap.getSingleton().getMimeTypeFromExtension(it) } + 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 ->