FileProvider是ContentProvider的一个特殊子类,它通过为文件创建一个content://的URI,而不是file:///URI,来帮助应用间的文件安全分享。在Android 7.0及以上的系统中,尝试传递file:///URI可能会触发FileUriExposedException。
一个content URI允许授权一个临时读写访问权限,当创建一个包含content URI的Intent时,就可以向其setFlags来增加权限。只要接收Activity的栈还存活,那么这个权限就是可用的,对于发送到Service的Intent,只要这个Service在运行,那么权限就一直是可用的。
相比之下,为了控制file:///URI的访问,就不得不修改底层文件的文件系统权限。在改变权限之前,它会对任何一个应用来说都是可用的,这种级别的访问基本是不安全的。
content URI 提供的文件访问安全级别的提高,使FileProvider成为Android安全基础设施的关键部分。
1. 定义一个FileProvider
FileProvider已经包含了基本的content URI生成功能,所以就不用再定义它的子类了,直接在XML中指定一个<provider>
即可,其中android:name
是固定的,androidx.core.content.FileProvider
,android:authorities
则是自己的域名加上fileprovider,android:exported
设置为false,android:grantUriPermissions
设置为true
1 | <manifest> |
如果想重写FileProvider的行为,则继承FileProvider类,然后用全类名替换<provider>
的name属性,即可
2. 指定可用的文件
FileProvider只能生成提前指定的目录下的文件的content URI。要想指定一个目录,则需要通过使用<paths>
标签,在xml中指定它的存储区域和路径。下面这个例子,则是告诉FileProvider想要请求私有文件区域下名字为images的子目录的content URI
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
<paths>
元素下,必须包含一个,或者多个<files-path>
元素
以下是所有可用的文件区域
- Context.getFilesDir()下的子目录
1
<files-path name="name" path="path" />
- Context.getCacheDir()下的子目录
1
<cache-path name="name" path="path" />
- Environment.getExternalStorageDirectory()下的子目录
1
<external-path name="name" path="path" />
- Context.getExternalFilesDir(String)或者Context.getExternalFilesDir(null)下的子目录
1
<external-files-path name="name" path="path" />
- Context.getExternalCacheDir()下的子目录
1
<external-cache-path name="name" path="path" />
- Context.getExternalMediaDirs()下的子目录,注意,这个目录只在API 21+的设备上可用
1
<external-media-path name="name" path="path" />
name="name"
URI的路径片段,为了保证安全,子目录的名字被隐藏在了path
属性中path="path"
分享的子目录的名字,name属性只是一个片段,path属性是真实的子目录名字。要注意,这个名字代表的是一个子目录的名字,而不是文件的名字,不允许通过名字分享一个单独的文件,或者使用通配符指定一个文件集合。
必须为每个子目录指定一个<paths>
元素,如下就是指定了两个目录
1 | <paths xmlns:android="http://schemas.android.com/apk/res/android"> |
将<paths>
元素和它的子元素写到一个XML文件里,放到项目中,例如可以这样做,res/xml/file_paths.xml
,然后将这个文件设置给<provider>
,像下面这样
1 | <provider |
3. 为文件生成content URI
为了把文件分享给其他应用,你需要生成一个content URI,就要通过FileProvider的getUriForFile方法获取一个URI,然后通过Intent传给其他应用,当其他应用拿到这个URI后,通过ContentResolver.openFileDescriptor就可以获得一个ParcelFileDescriptor
1 | File imagePath = new File(Context.getFilesDir(), "my_images"); |
前面的结果,就是得到一个content://com.mydomain.fileprovider/my_images/default_image.jpg
的URI
4. 授予URI临时权限
可以把权限授予给一个指定的package,也可以在Intent中包含权限。
调用方法Context.grantUriPermission(package, Uri, mode_flags)来为content URI授予权限,mode_flags可以设为FLAG_GRANT_READ_URI_PERMISSION或者FLAG_GRANT_WRITE_URI_PERMISSION,也可以设置两个flag。这个权限会一直有效,直到调用了revokeUriPermission()方法,或者设备重启。
当想把权限包含在Intent中时,
- 调用Intent.setData()把URI设给Intent
- 调用Intent.setFlags设置FLAG_GRANT_READ_URI_PERMISSION、FLAG_GRANT_WRITE_URI_PERMISSION,或者两个都设置。为了兼容4.1(API 16)和5.1(API 22)之间的设备,包括5.1,使用ClipData
1
2shareContentIntent.setClipData(ClipData.newRawUri("", contentUri));
shareContentIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); - 将Intent发给其他应用,通常可用调用setResult()来完成
Intent里权限在接收Activity栈活跃前都是可用的,当栈完成被销毁后,权限自动移除。授予给某个应用的一个Activity的权限,会自动扩展至该应用的其他组件。
5. 将content URI提供给另一个应用
有多种方式可讲content URI提供给客户端应用。一个常用的方式,客户端应用通过Intent启动你的应用里的一个Activity。作为回应,你的应用应该立即返回一个content URI给客户端应用,或者展示一个页面允许用户选择一个文件,这种情况下,一旦用户选择了一个文件,你的应用就可以返回这个文件的URI。在这两种情况中,你的应用都是通过setResult()返回一个Intent。
还可以把content URI放到一个ClipData对象中,然后将这个对象添加到发送给客户端应用的Intent中,可通过调用Intent.setClipData()实现。当使用这种方式时,可添加多个ClipData对象到Intent中,每一个都有着它自己的URI,当通过Intent.setFlags()设置访问权限时,所有的content URI都将获得同样的权限。
注意,Intent.setClipData()只在API 16及之后的平台上可用。如果需要在更早版本上的兼容性,应该一次在一个Intent里只发送一个conent URI。设置Intent的action为ACTION_SEND,然后通过调用setData()来添加URI。
6. 总结
妈呀,终于轮到我说话了。上面这些都是翻译的官方文档,本想用Google翻译一键转换,发现Google翻译并不是太懂Android官方写的文档,虽然Android也是Google的,同进一家门,不认自家人,所以只要自己动手,一句一句来,太累了。
感觉老外写文档很详细,用简短的话来概括就是,为了安全,所以官方不再让用file:///,而是用content://来替代,因为后者可以更好的管理访问授权,要想分享文件,就要先把文件所在目录写到一个xml里,然后再提供一个FileProvider,如果没有特殊需求就用官方的就可以,然后后面的事,通过Intent来操作了。
最后附上文档地址。