自7.0开始,android不允许应用之间通过file://协议的Uri共享文件,否则将抛出FileUriExposedException异常。举个常见的使用场景:调用系统应用拍照,我们需要传递一个Uri,告诉照片的存储位置。
Intent capturePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File destinationFile = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "my_pic.jpg");
Uri savePicFileUri = Uri.fromFile(destinationFile);
capturePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, savePicFileUri);
startActivity(capturePicIntent);
在7.0及以后,如此创建Uri会抛出异常。
调取系统相机拍照的本质是与系统应用交互,而不同应用之间不允许通过file://协议的Uri共享文件。Android给出的解决方案是通过FileProvider生成一个content://协议的Uri。
第一步,继承android.support.v4.content.FileProvider定义一个FileProvider类,但不需要如外实现任何代码。大多数开发者会直接使用android.support.v4.content.FileProvider,不推荐那么做,原因后面剖析。
package com.coder.zzq.system.version.sdk.adaptation.provider;
public class MyFileProvider extends android.support.v4.content.FileProvider {
}
第二步,声明FileProvider,FileProvider本质是一个ContentProvider,所以要在Manifest.xml文件中声明。
<provider
android:name=".provider.MyFileProvider"
android:authorities="com.coder.zzq.system.version.sdk.adaptation.FILE_PROVIDER"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/mapping_file_path" />
</provider>
<provider>标签下,
- name,FileProvider的全称类名
- authorities,FileProvider的唯一标识,一般以包名打头,确保唯一性
- exported,false表示不公开给其他应用
- android:grantUriPermissions,true表示授予Uri临时权限
<meta>标签下,
- name,固定值,"android.support.FILE_PROVIDER_PATHS",该项meta-data的名称
- resource,路径映射文件
第三步,编写路径映射文件,路径映射文件用于配置其他应用通过FileProvider可访问的目录。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="external_storage_files_dir"
path="/Pictures" />
</paths>
paths提供了6个子标签,用于配置可访问的路径类型。
- <file-path>,根路径对应内部存储:/data/user/0/包名/files
- <cache-path> 根路径对应内部存储:/data/user/0/包名/cache
- <external-path> 根路径对应外部存储:storage/emulated/0
- <external-files-path> 根路径对应外部存储:/storage/emulated/0/Android/data/包名/files
- <external-cache-path> 根路径对应外部存储:/storage/emulated/0/Android/data/包名/cache
- <external-media-path> 根路径对应外部存储:/storage/emulated/0/Android/media/包名
每个子标签的name属性表示该项配置的名字,可自由设置,path为子路径,如果是该路径类型下所有文件及子路径均可访问,则用“.”表示。
第四步,使用FileProvider创建Uri,
Intent capturePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File destinationFile = new File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "my_pic.jpg");
Uri savePicFileUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
savePicFileUri = MyFileProvider.getUriForFile(this, getString(R.string.file_provider_authorities), destinationFile);
} else {
savePicFileUri = Uri.fromFile(destinationFile);
}
capturePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, savePicFileUri);
startActivity(capturePicIntent);
至此,适配完成。
最后说一下为什么不推荐直接使用android.support.v4.content.FileProvider而是多此一举创建一个没有任何新代码的子类。
我们的项目都会引入第三方库,在第三方库中由于功能需要,可能也进行了FileProvider的配置。假设这个第三方库就是用来拍照的,它封装了拍照功能,实现上把照片的Uri直接置于内部存储的/data/user/0/包名/files/Pictures下。它的路径映射文件如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="internal_storage_files_dir"
path="." />
</paths>
封装代码如下:
public class CaptureUtils {
public static void capture(Context context) {
Intent capturePicIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
File destinationFile = new File(context.getFilesDir(), "my_pic.jpg");
Uri savePicFileUri = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
savePicFileUri = FileProvider.getUriForFile(context, context.getString(R.string.file_provider_authorities), destinationFile);
} else {
savePicFileUri = Uri.fromFile(destinationFile);
}
capturePicIntent.putExtra(MediaStore.EXTRA_OUTPUT, savePicFileUri);
context.startActivity(capturePicIntent);
}
}
我们的项目和该库都直接使用android.support.v4.content.FileProvider类的话,那么我们的项目将编译报错。
清单文件合并失败,因为同一个FileProvider实现类,存在两个authorities,提示我们用tools语法进行replace。
我们replace一下,
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.coder.zzq.system.version.sdk.adaptation.FILE_PROVIDER"
android:exported="false"
android:grantUriPermissions="true"
tool:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/mapping_file_path" />
</provider>
然后编译继续报错。
该库的映射文件名和我们不一致,我们继续按照提示replace。
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.coder.zzq.system.version.sdk.adaptation.FILE_PROVIDER"
android:exported="false"
android:grantUriPermissions="true"
tool:replace="android:authorities">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/mapping_file_path"
tool:replace="android:resource"/>
</provider>
然后在调用CaptureUtils.capture(this)的地方依然编译报错。
因为我们的路径映射文件中,并不包含第三方库路径映射文件的配置范围。现在我们把它加进来。
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-files-path
name="external_storage_files_dir"
path="/Pictures" />
<files-path
name="internal_storage_files_dir"
path="." />
</paths>
最后终于通过了编译。
这说明,我们的项目和第三方库都直接使用android.support.v4.content.FileProvider类的话,我们需要replace它们的authorities和resource,并且把所有第三方库映射文件中的配置项都要包含到我们项目的映射文件中来。非常麻烦且容易疏漏。最好的办法就是自定义FileProvider类,使我们的FileProvider同其他的库的FileProvider分开,互不干扰。由于不需要如外实现新的代码,所以是很简单的。