Android 7.0使用FileProvider解决file:// URI引起的FileUriExposedException异常

现象描述

Android 7.0以前的版本

1
2
3
4
Uri photoUri = Uri.fromFile(tempFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(cameraIntent, REQUEST_CODE_CAMERA);

File文件直接转换成”file://xxx/xxx/xxx”的uri格式

Android 7.0及以后的版本

当把targetSdkVersion指定成24及以上并且在API>=24的设备上运行时,这种方式则会出现FileUriExposedException异常

1
android.os.FileUriExposedException: file:///storage/emulated/0/europa/DCIM/Camera/0_42_20180908_123018_review.jpg exposed beyond app through ClipData.Item.getUri()

产生原因

参考:https://developer.android.com/reference/android/os/FileUriExposedException.html

Android不再允许在App中把file://Uri暴露给其他App,包括但不局限于通过Intent或ClipData等方法。

原因在于使用file://Uri会有一些风险,比如:

  • 文件是私有的,接收file://Uri的App无法访问该文件。
  • 在Android 6.0之后引入运行时权限,如果接收file://Uri的App没有申请READ_EXTERNAL_STORAGE权限,在读取文件时会引发崩溃。

因此,Google提供了FileProvider,使用它可以生成content://Uri来替代file://Uri。

解决方案

首先在AndroidManifest.xml中添加provider

  • android:authorities 用来标识provider的唯一标识,在同一部手机上一个”authority”串只能被一个App使用,冲突的话会导致App无法安装。
  • android:exported 必须设置成false
  • android:grantUriPermissions 用来控制共享文件的访问权限,也可以在Java代码中设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="me.yezhou.lib.photo" >

<application>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>

</manifest>

res/xml/provider_paths.xml 是指定路径和转换规则,如

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<resources>
<paths>
<external-cache-path path="photos/camera" name="camera_photos" />
<cache-path path="photos/camera" name="camera_photos" />
</paths>
</resources>

中可以定义以下子节点

子节点 对应路径 例子
files-path Context.getFilesDir()
cache-path Context.getCacheDir()
external-path Environment.getExternalStorageDirectory() /storage/emulated/0/
external-files-path Context.getExternalFilesDir(null)
external-cache-path Context.getExternalCacheDir()

假如要将目录 file:///storage/emulated/0/appblog.cn/photo/ 替换为 content://${android:authorities}/photo_files/

那么配置应该写成

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path path="appblog.cn/photo" name="photo_files" />
</paths>

然后修改代码

1
2
3
4
5
6
7
8
//Uri photoUri = Uri.fromFile(tempFile);
Uri photoUri = FileProvider.getUriForFile(
mContext,
mActivity.getPackageName() + ".fileprovider",
tempFile);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
startActivityForResult(cameraIntent, REQUEST_CODE_CAMERA);

常见异常处理

1
java.lang.SecurityException: Provider must not be exported

解决方案:android:exported必须设置成false

1
Attempt to invoke virtual method 'android.content.res.XmlResourceParser android.content.pm.PackageItemInfo.loadXmlMetaData(android.content.pm.PackageManager, java.lang.String)' on a null object reference

解决方案:AndroidManifest.xml处的android:authorities必须跟mActivity.getPackageName() + ".fileprovider"一致

FileProvider

FileProvider使用content://Uri的优点

  • 可以控制共享文件的读写权限,只要调用Intent.setFlags()就可以设置对方App对共享文件的访问权限,并且该权限在对方App退出后自动失效。相比之下,使用file://Uri时只能通过修改文件系统的权限来实现访问控制,访问控制是对所有App都生效的,不能区分App。
  • 可以隐藏共享文件的真实路径。

file://content://的转换规则

  • 替换前缀:把file://替换成content://${android:authorities}
  • 匹配和替换:遍历的子节点,找到最大能匹配path的子节点,用name替换掉文件路径里所匹配的内容

设置文件的访问权限

有两种设置权限的办法:

  • 调用Context.grantUriPermission(package, uri, modeFlags)。这样设置的权限只有在手动调用Context.revokeUriPermission(uri, modeFlags)或系统重启后才会失效。
  • 调用Intent.setFlags()来设置权限。权限失效的时机:接收Intent的Activity所在的stack销毁时。

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :