12、文件 Provider:FileProvider 使用、文件 URI 生成、临时权限授予
聊到 FileProvider,我得先坦白一件事。刚做 Android 那会儿,我直接拿 file:// 路径传给别的 App,结果在 Android 7.0 上直接崩了。嗯,就是那个著名的 FileUriExposedException。当时我还在想,Google 怎么这么不近人情?后来才明白,这是为了安全。
说白了,FileProvider 就是 Android 官方给咱们准备的一个「安全文件快递员」。它能把 App 内部的文件,包装成一个 content:// 的 URI 发给别人。对方拿着这个 URI,就能在临时权限范围内读写文件。
12.1 为什么需要 FileProvider?
你想想看,如果直接把 file:///storage/emulated/0/... 发给第三方 App,对方就能拿到文件的绝对路径。这太危险了。恶意 App 完全可以顺着路径去偷你别的文件。
FileProvider 做了两件事:
- 隐藏真实路径——把
/data/data/你包名/files/xxx.jpg映射成content://你包名.fileprovider/files/xxx.jpg - 控制访问权限——只有你授权的 App,在授权时间内才能读写
我在项目中遇到过一件事:有个第三方 SDK 要读取我们 App 的日志文件。如果直接给 file URI,那 SDK 就能遍历整个 data 目录。用了 FileProvider 之后,它只能看到我授权的那一个文件。安全多了。
12.2 配置 FileProvider
配置其实不复杂,分三步走。
12.2.1 在 AndroidManifest 中声明
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
这里有个细节要注意:android:exported="false" 是必须的。因为 FileProvider 不需要被外部直接调用,它只通过 Intent 来工作。我见过有人手抖写成 true,虽然也能用,但安全审计会报警。
12.2.2 编写 file_paths.xml
在 res/xml/ 目录下创建这个文件。它定义了哪些路径可以被映射。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="my_files" path="." />
<cache-path name="my_cache" path="." />
<external-path name="external_files" path="." />
<external-files-path name="external_app_files" path="." />
<external-cache-path name="external_cache" path="." />
</paths>
每个标签对应一种存储区域:
| 标签 | 对应路径 | 适用场景 |
|---|---|---|
<files-path> |
Context.getFilesDir() |
内部存储私有文件 |
<cache-path> |
Context.getCacheDir() |
缓存文件 |
<external-path> |
Environment.getExternalStorageDirectory() |
外部存储根目录 |
<external-files-path> |
Context.getExternalFilesDir() |
外部存储私有目录 |
<external-cache-path> |
Context.getExternalCacheDir() |
外部缓存目录 |
<files-path> 和 <cache-path>。外部存储的路径太宽泛,容易暴露不该暴露的东西。除非业务确实需要,否则别偷懒全写上。
12.3 生成文件 URI
配置好了之后,生成 URI 就一行代码的事:
File file = new File(context.getFilesDir(), "report.pdf");
Uri contentUri = FileProvider.getUriForFile(context,
context.getPackageName() + ".fileprovider",
file);
返回的 URI 长这样:
content://com.example.myapp.fileprovider/my_files/report.pdf
注意看,my_files 就是你在 file_paths.xml 里定义的 name 属性。它把真实路径给隐藏了。
我曾经踩过一个坑:getUriForFile 的第二个参数必须和 Manifest 里的 android:authorities 完全一致。少写个点都不行。而且如果你用了 applicationId 占位符,那这个参数最好动态拼接,别写死。
12.4 临时权限授予
光生成 URI 还不够。你得告诉系统:「我允许哪个 App 来读/写这个文件」。这就是临时权限的作用。
12.4.1 通过 Intent 授权
最常见的做法,是在发送 Intent 时加上 flag:
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(contentUri, "application/pdf");
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// 如果需要写权限,再加这个
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivity(intent);
这里有两个 flag:
FLAG_GRANT_READ_URI_PERMISSION——授予读权限FLAG_GRANT_WRITE_URI_PERMISSION——授予写权限
权限是「临时」的,什么意思呢?就是目标 App 在它的 Activity 存活期间可以访问。一旦进程被杀,权限就没了。你想想看,这比直接给文件路径安全多了吧?
12.4.2 通过 setClipData 授权
如果你要发送多个文件,可以用 ClipData:
Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("image/*");
ArrayList<Uri> uris = new ArrayList<>();
uris.add(FileProvider.getUriForFile(...));
uris.add(FileProvider.getUriForFile(...));
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
intent.setClipData(ClipData.newUri(getContentResolver(), "images", uris.get(0)));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
这里有个关键点:setClipData 会把所有 URI 都带上,系统会自动给这些 URI 都授予权限。如果你只调 addFlags 但不设 setClipData,那只有第一个 URI 有权限。嗯,这个细节我当年也是 debug 了好久才发现的。
12.5 知识体系图
下面这张图总结了 FileProvider 的完整工作流程:
12.6 避坑指南
最后分享几个我实战中遇到的坑:
- Android 7.0 以上必须用——低于 7.0 的设备虽然不会崩溃,但为了统一,我建议全版本都用 FileProvider。别搞版本判断。
- 路径别写太宽——
path="."表示整个目录都暴露。如果你只想暴露某个子目录,写成path="downloads/"更安全。 - 临时权限不是永久的——如果目标 App 想长期持有这个文件,它应该自己复制一份。别指望 URI 权限能跨进程持久化。
- 多文件发送记得用 ClipData——我前面说了,不加 ClipData 的话,只有第一个 URI 有权限。这个坑我至少见过三次。
好了,FileProvider 的核心内容就这些。说白了就是三步:配置、生成 URI、授权。但每一步都有细节,不注意就会踩坑。希望你能在实际项目中用起来,别再被 FileUriExposedException 折磨了。
公众号:蓝海资料掘金营,微信deep3321