oynix

于无声处听惊雷,于无色处见繁花

Android之FileProvider

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.FileProviderandroid:authorities则是自己的域名加上fileprovider,android:exported设置为false,android:grantUriPermissions设置为true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<manifest>
...
<application>
...
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
...
</provider>
...
</application>
</manifest>

如果想重写FileProvider的行为,则继承FileProvider类,然后用全类名替换<provider>的name属性,即可

2. 指定可用的文件

FileProvider只能生成提前指定的目录下的文件的content URI。要想指定一个目录,则需要通过使用<paths>标签,在xml中指定它的存储区域和路径。下面这个例子,则是告诉FileProvider想要请求私有文件区域下名字为images的子目录的content URI

1
2
3
4
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
...
</paths>

<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
2
3
4
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
<files-path name="my_docs" path="docs/"/>
</paths>

<paths>元素和它的子元素写到一个XML文件里,放到项目中,例如可以这样做,res/xml/file_paths.xml,然后将这个文件设置给<provider>,像下面这样

1
2
3
4
5
6
7
8
9
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.mydomain.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>

3. 为文件生成content URI

为了把文件分享给其他应用,你需要生成一个content URI,就要通过FileProvider的getUriForFile方法获取一个URI,然后通过Intent传给其他应用,当其他应用拿到这个URI后,通过ContentResolver.openFileDescriptor就可以获得一个ParcelFileDescriptor

1
2
3
File imagePath = new File(Context.getFilesDir(), "my_images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);

前面的结果,就是得到一个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
    2
    shareContentIntent.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来操作了。

最后附上文档地址

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/04/5a1944b34d5b/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道