如何在不请求 MANAGE_EXTERNAL_STORAGE 权限的情况下访问共享文件?
解读
面试官问的是“共享文件”,在国内面试语境里通常指:
- 用户主动挑选的照片、视频、音频、文档(媒体库或非媒体文件);
- 其他应用通过 FileProvider 暴露给本应用的文件;
- 下载目录、文档目录等公共目录里的文件。
MANAGE_EXTERNAL_STORAGE(俗称“所有文件访问权限”)在 Android 11 及以上属于“特殊权限”,上架国内应用商店(华为、小米、OPPO、vivo、应用宝)时会被重点审查,若无“核心功能必要”理由会被直接驳回。因此“不请求该权限”是合规上架的硬需求,也是面试考察“Scoped Storage”适配能力的核心点。
知识点
- Scoped Storage 分区存储模型(Android 10 强制开启,Android 11 全量生效)。
- MediaStore API:通过 ContentResolver 查询 MediaStore.Files/Images/Video/Audio,拿到 content:// 类型的 Uri,即可 openFileDescriptor/openInputStream 进行读写;无需任何存储权限即可访问“自己创建”的文件,访问“他人创建”的文件只需 READ_EXTERNAL_STORAGE。
- Storage Access Framework(SAF):ACTION_OPEN_DOCUMENT / ACTION_CREATE_DOCUMENT,由系统文件选择器返回 Uri,通过 ContentResolver 打开 FD 或流,无需存储权限,可读写任何用户可见目录。
- FileProvider + Intent.FLAG_GRANT_READ_URI_PERMISSION:跨应用共享私有文件,接收方在 Intent 中拿到 Uri,无需任何存储权限。
- DownloadManager:下载到公共下载目录,通过 query 拿到本地 Uri,直接访问。
- App-specific 目录:getExternalFilesDir()、getExternalCacheDir() 下的文件不受 Scoped Storage 限制,但属于包名隔离,不算“共享”。
- 权限对比:READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE 在 Android 13 已被细化为 READ_MEDIA_IMAGES/VIDEO/AUDIO;以上权限仍可在不申请 MANAGE_EXTERNAL_STORAGE 的前提下使用,但需按需声明。
- 国内合规要点:华为/小米应用商店检测工具会扫描 manifest 中是否声明 MANAGE_EXTERNAL_STORAGE,若声明则必须上传“权限使用申请表”,否则无法过审。
答案
分场景给出“零 MANAGE_EXTERNAL_STORAGE”访问共享文件的标准做法:
-
访问公共媒体文件(图片、视频、音频)
- 仅声明 READ_EXTERNAL_STORAGE(Android 13 以下)或 READ_MEDIA_IMAGES/VIDEO/AUDIO(Android 13+)。
- 通过 ContentResolver 查询 MediaStore,获得 content://media/external/images/media/xxx 类型的 Uri。
- 使用 contentResolver.openInputStream(uri) 即可读取;写入时先调用 ContentValues 插入新记录,再 contentResolver.openOutputStream(uri) 写入,无需 WRITE_EXTERNAL_STORAGE。
-
访问非媒体公共文档(PDF、DOC、XLS 等)
- 使用 Storage Access Framework:
- 读:startActivityForResult(Intent(ACTION_OPEN_DOCUMENT), …),用户在系统文件选择器里手动选择,返回 Uri,通过 ContentResolver 打开 FD 或流。
- 写:startActivityForResult(Intent(ACTION_CREATE_DOCUMENT), …),用户指定文件名与路径,返回 Uri,再 openOutputStream。
- 全程无需任何存储权限,系统已授予临时读写授权。
- 使用 Storage Access Framework:
-
接收其他应用主动共享的文件
- 发送方通过 FileProvider 生成 content:// 类型的 Uri,Intent 中加入 FLAG_GRANT_READ_URI_PERMISSION。
- 接收方在 AndroidManifest 中只声明 <intent-filter><action android:name="android.intent.action.SEND" …/></intent-filter> 即可直接通过 getParcelableExtra(Intent.EXTRA_STREAM) 拿到 Uri,用 ContentResolver 访问,无需任何存储权限。
-
访问自己下载的共享文件
- 使用系统 DownloadManager 下载到公共下载目录,下载完成后通过 query 拿到本地 Uri,直接 openInputStream,无需额外权限。
-
需要批量迁移旧版 /sdcard/ 绝对路径代码
- 将 File(path) 读写改为 ContentResolver + Uri 方式;
- 若必须兼容 Android 9 及以下,可在 manifest 中声明 requestLegacyExternalStorage="true",但 Android 11 以上自动失效,仍需走上述 Uri 方案。
结论:只要遵循“Uri + ContentResolver”或“系统文件选择器”两条主线,就完全不需要 MANAGE_EXTERNAL_STORAGE 即可读写所有用户可见的共享文件,同时满足国内商店合规要求。
拓展思考
- Android 14 开始,MediaStore 支持“照片选择器” Photo Picker,调用 androidx.activity:activity:1.7+ 的 ActivityResultContracts.PickVisualMedia(),连 READ_MEDIA_IMAGES 权限都不需要即可读取用户选中的照片,未来将成为主流。
- 对于企业级网盘、文件管理类应用,若确实需要“全盘文件”能力,可在国内商店申请“核心功能原因”走白名单,但需提供用户协议、隐私政策、功能演示视频及工信部备案,审核周期 2–4 周;面试时可展示自己如何准备材料、如何降级方案(SAF + MediaStore 双通道)以保证审核失败仍可发版。
- 性能优化点:SAF 返回的 Uri 并不保证底层是本地文件,可能是云端盘(Google Drive、OneDrive),直接 openInputStream 可能触发网络请求;面试可补充“通过 DocumentsContract 判断 FLAG_SUPPORTS_THUMBNAIL / FLAG_LOCAL_ONLY,再决定是否需要缓存到本地,避免主线程阻塞”。
- 安全细节:接收方拿到 Uri 后应使用 ContentResolver.takePersistableUriPermission() 持久化授权,防止进程被杀后失去读写权;同时要在使用完后 revokeUriPermission,防止权限泄漏。
- 车载、Wear 等特殊设备:部分 OEM 把 SAF 裁剪掉,此时只能回退到 MediaStore 或 OEM 提供的私有 SDK;面试可展示自己如何通过 PackageManager 判断系统是否支持 ACTION_OPEN_DOCUMENT,动态降级方案体现“兼容性思维”。