ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Android 文档扫描不到本地文件?用 MediaStore 与 contentResolver 排查 mime_type 配置

Android 文档扫描不到本地文件?用 MediaStore 与 contentResolver 排查 mime_type 配置 1. 文档扫描不到本地文件问题到底出在哪Android 上做文档扫描很多人第一反应是遍历Environment.getExternalStorageDirectory()自己递归找文件。这条路在 Android 10 之后基本走不通了分区存储把直接路径访问卡得很死。于是大家转向MediaStorecontentResolver查询系统媒体库结果又遇到一个更隐蔽的坑图片、视频能查出来.docx、.xlsx、.rar这些文档却一个都扫不到。这个现象的本质不是权限没给够也不是文件不存在而是MediaStore 的索引机制和 mime_type 过滤规则在作怪。系统媒体库对文件的收录和分类依赖MIME_TYPE字段而很多办公文档在写入时MIME_TYPE是空的或者被归到了一个很窄的分类里。你用MediaStore.Files.getContentUri(external)去查如果不显式指定筛选条件默认拿到的往往是系统认为可识别的那部分文件.docx、.xls、.rar这类就被漏掉了。适合读这篇的人正在做 Android 文件管理、文档扫描、附件选择器、办公类 App 的开发者尤其是已经用了 MediaStore 但发现文档类文件查不全的同学。下面我会从索引机制讲起给出可直接复制的查询代码、mime_type 白名单骨架以及用 adb 和日志验证扫描结果的具体动作帮你确认文件到底有没有被系统媒体库收录。2. 先搞清楚 MediaStore 的索引与 mime_type 机制2.1 MediaStore 不是文件系统它是一张数据库表很多人把 MediaStore 当成能列出所有文件的接口其实它只是MediaProvider维护的一组数据库表。文件要被查出来前提是它已经被扫描并插入到对应的表里。图片进Images表视频进Video表音频进Audio表而其他杂项文件进Files表。MediaStore.Files是个兜底表理论上所有被媒体扫描器MediaScanner处理过的文件都会在这里有一条记录。但问题在于扫描器是否愿意收录一个文件取决于它能不能识别这个文件的类型。识别依据就是文件扩展名和MIME_TYPE。2.2 mime_type 为空文件就等于隐身当系统扫描到一个.docx文件时会尝试通过MimeTypeMap推断它的 MIME 类型。如果推断失败MIME_TYPE字段可能就是null或者application/octet-stream。这时候如果你查询时带了mime_type?这样的条件或者依赖系统默认的分类过滤这个文件就不会出现在结果里。更麻烦的是不同厂商 ROM 对办公文档的 MIME 映射表不一样。同一份.xlsx在原生 Android 上可能被识别为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet在某些定制系统上可能直接是空。这就是为什么同样的代码换个手机就扫不到。2.3 用 DATA 字段做扩展名匹配是可靠的兜底既然MIME_TYPE不可靠那就绕开它直接用文件路径的扩展名来筛选。MediaStore.Files.FileColumns.DATA字段存的是文件的绝对路径在分区存储下虽然不推荐直接读文件但作为查询条件仍然可用。通过DATA LIKE %.docx这样的条件可以把系统没正确分类的文档也捞出来。这就是下面代码的核心思路不依赖 mime_type而是构造一个基于扩展名的 selection 字符串。3. 可复制的 MediaStore 查询与 mime_type 白名单配置3.1 构造办公文档的 selection 条件先定义一个方法把所有需要支持的文档扩展名拼成 SQL 的OR条件。注意这里用的是DATA字段做LIKE匹配覆盖 Word、Excel、PowerPoint、Project、Visio、Access 以及压缩包。/** * 构造办公文档的查询条件 * 通过 DATA 字段的扩展名匹配绕开 mime_type 识别不准的问题 */ private static String buildOfficeSelectionStr() { return ( // Excel MediaStore.Files.FileColumns.DATA LIKE %.xls or MediaStore.Files.FileColumns.DATA LIKE %.xlsx or MediaStore.Files.FileColumns.DATA LIKE %.xlsm or MediaStore.Files.FileColumns.DATA LIKE %.xltx or MediaStore.Files.FileColumns.DATA LIKE %.xltm or MediaStore.Files.FileColumns.DATA LIKE %.xlam // PowerPoint or MediaStore.Files.FileColumns.DATA LIKE %.pptx or MediaStore.Files.FileColumns.DATA LIKE %.pptm or MediaStore.Files.FileColumns.DATA LIKE %.potm or MediaStore.Files.FileColumns.DATA LIKE %.potx or MediaStore.Files.FileColumns.DATA LIKE %.ppsx or MediaStore.Files.FileColumns.DATA LIKE %.ppsm or MediaStore.Files.FileColumns.DATA LIKE %.sldx or MediaStore.Files.FileColumns.DATA LIKE %.thmx or MediaStore.Files.FileColumns.DATA LIKE %.ppt // Word or MediaStore.Files.FileColumns.DATA LIKE %.dotm or MediaStore.Files.FileColumns.DATA LIKE %.docx or MediaStore.Files.FileColumns.DATA LIKE %.docm or MediaStore.Files.FileColumns.DATA LIKE %.dotx or MediaStore.Files.FileColumns.DATA LIKE %.doc // Project or MediaStore.Files.FileColumns.DATA LIKE %.mpp // Visio or MediaStore.Files.FileColumns.DATA LIKE %.vsd // Access or MediaStore.Files.FileColumns.DATA LIKE %.mdb or MediaStore.Files.FileColumns.DATA LIKE %.mde or MediaStore.Files.FileColumns.DATA LIKE %.accdb // 压缩包 or MediaStore.Files.FileColumns.DATA LIKE %.rar ); }3.2 执行查询并读取结果有了 selection接下来用contentResolver.query去查MediaStore.Files表。projection 里至少要带上_ID、DATA、DISPLAY_NAME、SIZE、MIME_TYPE方便后续展示和调试。public ListDocumentItem queryOfficeDocuments(Context context) { ListDocumentItem result new ArrayList(); Uri uri MediaStore.Files.getContentUri(external); String[] projection new String[]{ MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DATA, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.SIZE, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.DATE_MODIFIED }; String selection buildOfficeSelectionStr(); String sortOrder MediaStore.Files.FileColumns.DATE_MODIFIED DESC; Cursor cursor null; try { cursor context.getContentResolver().query( uri, projection, selection, null, sortOrder); if (cursor ! null) { int idIdx cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID); int dataIdx cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DATA); int nameIdx cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.DISPLAY_NAME); int sizeIdx cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.SIZE); int mimeIdx cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns.MIME_TYPE); while (cursor.moveToNext()) { DocumentItem item new DocumentItem(); item.id cursor.getLong(idIdx); item.path cursor.getString(dataIdx); item.name cursor.getString(nameIdx); item.size cursor.getLong(sizeIdx); item.mimeType cursor.getString(mimeIdx); result.add(item); } } } catch (Exception e) { Log.e(DocScan, query office documents failed, e); } finally { if (cursor ! null) { cursor.close(); } } return result; }3.3 mime_type 白名单配置骨架如果你更倾向于用MIME_TYPE做筛选比如做附件选择器时想按类型分组可以维护一份白名单。但记住白名单只能作为辅助不能作为唯一条件因为很多文档的MIME_TYPE是空的。public static final SetString OFFICE_MIME_WHITELIST new HashSet(Arrays.asList( // Word application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document, application/vnd.ms-word.document.macroenabled.12, // Excel application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, application/vnd.ms-excel.sheet.macroenabled.12, // PowerPoint application/vnd.ms-powerpoint, application/vnd.openxmlformats-officedocument.presentationml.presentation, // PDF application/pdf, // 压缩包 application/x-rar-compressed, application/zip, application/x-7z-compressed ));查询时可以这样组合先用扩展名 selection 捞全量再用白名单在内存里做二次分类。这样既不会漏文件又能给用户一个清晰的类型标签。注意DATA字段在 Android 10 的分区存储下虽然还能作为查询条件但官方不保证长期可用。如果你的 App 目标版本较高建议同时保留MIME_TYPE白名单作为降级方案两条路一起走。4. 用 adb 与日志验证文件是否被媒体库收录代码写完了不代表文件就能查出来。很多时候是文件压根没被 MediaScanner 收录。这时候需要用 adb 直接查系统媒体库的数据库确认文件在不在。4.1 用 adb 查询 MediaStore 数据库连接设备后进入 shell找到媒体库数据库文件。不同 Android 版本路径略有差异常见的是/data/data/com.android.providers.media/databases/external.db。需要 root 权限才能直接读如果没有 root可以用content query命令走 ContentProvider。# 查询所有 docx 文件是否在媒体库中 adb shell content query --uri content://media/external/file \ --projection _id:_data:mime_type \ --where _data LIKE %.docx如果这条命令返回空说明系统媒体库里根本没有这个文件问题出在扫描环节不是你的查询代码。如果返回了记录但你的 App 查不到那才是 selection 或权限的问题。4.2 手动触发媒体扫描确认文件没被收录后可以手动触发一次扫描把文件推进媒体库。Android 提供了MediaScannerConnection也可以直接用 adb 广播。# 触发全盘媒体扫描部分 ROM 支持 adb shell am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE \ -d file:///sdcard/Download/test.docx在代码里更推荐用MediaScannerConnection.scanFile扫描完成后会回调可以拿到实际的contentUriMediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null, (path, uri) - Log.d(DocScan, scanned: path - uri));4.3 用日志确认查询结果在queryOfficeDocuments里加一行日志把 cursor 的 count 和每条记录的 mime_type 打出来。这样你能直观看到是查出来 0 条还是查出来了但 mime_type 是 null。Log.d(DocScan, cursor count (cursor null ? -1 : cursor.getCount())); // 循环里 Log.d(DocScan, name item.name , mime item.mimeType , path item.path);实测下来最常见的两种情况一是 cursor count 为 0说明文件没进媒体库需要触发扫描二是 count 正常但 mime_type 全是 null说明系统没识别出类型这时候你的扩展名 selection 就是救命的。5. 本篇常见错误排查5.1 查询返回空但文件明明存在先别怀疑代码用 4.1 的 adb 命令确认文件在不在媒体库。不在的话触发一次扫描再查。如果扫描后还是不在检查文件是不是放在Android/data或Android/obb目录下——这两个目录在 Android 11 对媒体扫描器是屏蔽的放这里的文件永远不会被收录。5.2 权限给了还是查不到Android 13 把存储权限拆成了READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO没有专门的文档权限。查MediaStore.Files里的文档需要的是READ_EXTERNAL_STORAGEAndroid 12 及以下或者MANAGE_EXTERNAL_STORAGE特殊场景。如果你只申请了READ_MEDIA_IMAGES文档是查不出来的。!-- Android 12 及以下 -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE / !-- Android 13 查文档仍需 READ_EXTERNAL_STORAGE 或走 SAF -- uses-permission android:nameandroid.permission.READ_EXTERNAL_STORAGE android:maxSdkVersion32 /5.3 selection 拼接导致 SQL 异常buildOfficeSelectionStr里如果某个扩展名写错或者括号不匹配query会直接抛IllegalArgumentException。建议在拼接后打印一下完整的 selection 字符串肉眼检查括号和or的位置。另外LIKE是大小写不敏感的.DOCX和.docx都能匹配不用额外处理。5.4 分区存储下 DATA 字段返回 nullAndroid 10 如果 App 没有requestLegacyExternalStorage查出来的DATA字段可能是 null。这时候展示文件名要用DISPLAY_NAME读取内容要用ContentResolver.openInputStream(uri)不要再去拼绝对路径。6. 接入与验证把查询能力落到实际工程里上面这套 MediaStore 查询逻辑单独跑通不难难的是在真实项目里稳定工作。如果你在做文档扫描类功能建议把查询、扫描触发、mime_type 分类封装成一个独立的DocumentRepository对外只暴露queryDocuments(type)这样的接口。调试阶段我习惯用 TaoToken 的模型对话能力来快速验证一些边界逻辑比如让它帮我检查 selection 字符串的括号是否匹配、mime_type 白名单有没有漏掉某个格式。它的 API 接入很直接把contentResolver查询的返回结构丢进去做结构化分析也方便。想快速验证查询逻辑和 mime_type 映射可以用模型对话直接问需要生成 API Key 接入到自己的调试脚本里去 API Keys 页面创建接入细节和参数说明看接入文档如果你在长期做 Android 编码类项目需要稳定的调用额度Coding Plan 更适合持续使用。最后留一个我踩过的坑MediaStore.Files查询在部分 ROM 上对.rar的收录率极低即使触发了扫描也可能不进去。如果你的 App 必须支持压缩包扫描建议对这类文件走 SAFStorage Access Framework让用户手动授权目录而不是死磕 MediaStore。两条路结合覆盖率才够。
返回列表