Android调用系统相机和相册
FileProvider
是
ContentProvider
的一个子类,它通过创建一种
content://
格式的
Uri
来加强应用之间共享文件的安全性,这是传统的
file:///
格式的
Uri
实现不了的。
content://
格式的
Uri
可以让我们为某个文件赋予让其他文件临时读写的权限,可以通过
Intent#addFlags()
方法添加权限,这些权限可以保留到目标
Activity
或
Service
被销毁。
相比之下,使用
file:///
格式的
Uri
是直接修改了底层文件系统的权限,除非手动修改,否则这些权限会一直存在,这是非常不安全的。
FileProvider
的出现解决了上述问题,提升了文件访问的安全性。
FileProvider
默认就具有生成
content://
格式的
Uri
的功能,因此我们不需要在代码中编写它的子类,几乎所有的代码都只需要在
XML
文件中进行配置即可。要配置
FileProvider
,我们首先需要在
AndroidManifest.xml
文件中通过
<provider>
标签进行声明,并配置自定义的
android:authorities
属性,代码如下:
<manifest>
<application>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="my.itgungnir.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
</provider>
</application>
</manifest>
上述代码中,
android:name
属性通畅配置为
android.support.v4.content.FileProvider
(如果当前项目基于
AndroidX
,则需要配置为
androidx.core.content.FileProvider
);
android:authorities
属性是当前应用的授权字符串,是其他应用访问当前应用中文件的凭证;
android:exported
属性表示该
FileProvider
是否是公有的,一般情况下都设为
false
;
android:grantUriPermissions
属性表示是否可以为文件赋予临时访问权限,一般情况下都设为
true
。如果我们想要扩展默认的
FileProvider
,则
android:name
属性需要配置为我们自定义的
FileProvider
类的全路径。
为了让其他应用可以访问当前应用下的文件,我们还需要配置哪些文件夹可以被访问,这个步骤也是在
XML
文件中配置的。我们需要在项目的
/res/xml
文件夹下创建一个路径配置文件,命名为
file_paths.xml
(文件名可以自定义),这个文件中的根节点是
<paths>
,在这个节点下配置文件夹。一个示例配置如下:
<?xml version="1.0" encoding="utf-8"?>
<paths>
<root-path name="root" path="my_root/" />
<files-path name="internal_files" path="my_internal_files/" />
<cache-path name="internal_cache" path="my_internal_cache/" />
<external-path name="external" path="my_external/" />
<external-files-path name="external_files" path="my_external_files/" />
<external-cache-path name="external_cache" path="my_external_cache/" />
<external-media-path name="external_media" path="my_external_media/" />
</paths>
这里支持七种标签,每种标签代表的父路径都通过注释的方式标记在上面,每种标签可以出现多次,表示同一个父路径下的多个文件夹。在每种标签中,
name
属性是这个文件夹的别名,这样可以保护文件夹的真是路径不外泄;
path
属性则是这个文件夹的真实名称。
需要注意的是,
file_paths.xml
文件中只能配置文件夹,不能配置单个文件;且一个
path
属性中只能配置一个文件夹,不能配置多个文件夹。
配置完各个文件夹之后,我们还需要在
AndroidManifest.xml
文件中的
<provider>
标签中引用这个配置,这样才能使配置生效。使用
<meta-data>
标签来引用配置,代码如下:
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="my.itgungnir.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
如上面的代码所示,
<meta-data>
标签中的
android:name
属性是固定值
android.support.FILE_PROVIDER_PATHS
,
android:resource
属性是对上面的配置文件的引用。
以上是对
FileProvider
的配置,下面介绍如何在代码中使用
FileProvider
共享文件。我们将通过点击页面中的一个按钮来调起相机,拍照后回传到页面中展示,并将照片存储在
External Storage
中的
my_external
文件夹中展示。我们首先需要获取到照片文件的
Uri
,代码如下:
val imgDir = File(Environment.getExternalStorageDirectory(), "my_external")
if (!imgDir.exists()) {
imgDir.mkdirs()
val imgFile = File(imgDir, "tmp_camera_capture.jpg")
if (!imgFile.exists()) {
imgFile.createNewFile()
uri = FileProvider.getUriForFile(this, "my.itgungnir.fileprovider", imgFile)
以上代码需要注意两点,第一,要共享的文件所在的文件夹必须要配置在
file_paths.xml
文件中,否则无法共享给其他应用,如上述代码中就是在别名为
external
的
my_external
文件夹中创建了一个
tmp_camera_capture.jpg
文件;第二,使用
FileProvider.getUriForFile
创建
Uri
的时候,第二个参数传递的是
AndroidManifest.xml
文件中
<provider>
标签的
android:authorities
对应的值。此处打印
uri.toString()
输出如下:
content://my.itgungnir.fileprovider/my_external/tmp_camera_capture.jpg
可以发现,使用
FileProvider
生成的
Uri
的格式为:
content://<授权字符串>/<文件夹别名>/<文件名>
可以看到,相比于通过传统的
imgFile.toUri()
或
Uri.fromFile(imgFile)
方式获取到的
Uri
(打印输出为
file:///storage/emulated/0/my_external/tmp_camera_capture.jpg
,使用的是文件的真实路径,全局可见,非常不安全),使用
FileProvider
的方式通过文件夹别名的方式隐藏了文件的真实路径,从而达到了保证文件安全的目的。
现在我们已经创建好了文件的
Uri
,下一步就是调用
Intent
去打开相机。这里相比传统的方式需要多写一些代码,我们需要手动的设置目标应用操作当前文件时可以行使的权限,代码如下:
Intent().apply {
action = MediaStore.ACTION_IMAGE_CAPTURE
putExtra(MediaStore.EXTRA_OUTPUT, uri)
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
this@MainActivity.startActivityForResult(this, 1)
可见,我们可以通过
Intent#addFlags()
方法添加临时权限,这里的权限有两种,分别是
Intent.FLAG_GRANT_READ_URI_PERMISSION
和
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
,分别表示读权限和写权限。
最后要做的就是在
onActivityResult()
方法中监听相机的返回值,并将结果图片加载到页面中:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == 1 && resultCode == RESULT_OK) {
imageResult.setImageURI(uri)
FileProvider原理分析
这里我们只分析在调用
FileProvider.getUriForFile()
方法时是如何生成
Uri
对象的。先看
getUriForFile()
方法的源码:
public static Uri getUriForFile(@NonNull Context context, @NonNull String authority,
@NonNull File file) {
final PathStrategy strategy = getPathStrategy(context, authority);
return strategy.getUriForFile(file);
可以看到,
getUriForFile
只是一个门面方法,实际生成
Uri
的操作是通过
PathStrategy
类来实现的。我们先进入
getPathStrategy()
方法看一下
PathStrategy
类是怎样生成的:
private static HashMap<String, PathStrategy> sCache = new HashMap<String, PathStrategy>();
private static PathStrategy getPathStrategy(Context context, String authority) {
PathStrategy strat;
synchronized (sCache) {
strat = sCache.get(authority);
if (strat == null) {
try {
strat = parsePathStrategy(context, authority);
} catch (IOException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
} catch (XmlPullParserException e) {
throw new IllegalArgumentException(
"Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e);
sCache.put(authority, strat);
return strat;
可见,
FileProvider
类中对
PathStrategy
对象做了一个简单的缓存,通过
authority
来存储不同的对象,当进入这个方法时,先判断缓存中有没有对应的对象,如果有则直接取出来使用,否则先创建一个,存储到缓存中并使用。接下来看一下创建对象的方法
parsePathStrategy()
方法的源码:
private static PathStrategy parsePathStrategy(Context context, String authority)
throws IOException, XmlPullParserException {
final SimplePathStrategy strat = new SimplePathStrategy(authority);
final ProviderInfo info = context.getPackageManager()
.resolveContentProvider(authority, PackageManager.GET_META_DATA);
final XmlResourceParser in = info.loadXmlMetaData(
context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS);
if (in == null) {
throw new IllegalArgumentException(
"Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data");
int type;
while ((type = in.next()) != END_DOCUMENT) {
if (type == START_TAG) {
final String tag = in.getName();
final String name = in.getAttributeValue(null, ATTR_NAME);
String path = in.getAttributeValue(null, ATTR_PATH);
File target = null;
if (TAG_ROOT_PATH.equals(tag)) {
target = DEVICE_ROOT;
} else if (TAG_FILES_PATH.equals(tag)) {
target = context.getFilesDir();
} else if (TAG_CACHE_PATH.equals(tag)) {
target = context.getCacheDir();
} else if (TAG_EXTERNAL.equals(tag)) {
target = Environment.getExternalStorageDirectory();
} else if (TAG_EXTERNAL_FILES.equals(tag)) {
File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null);
if (externalFilesDirs.length > 0) {
target = externalFilesDirs[0];
} else if (TAG_EXTERNAL_CACHE.equals(tag)) {
File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context);
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
&& TAG_EXTERNAL_MEDIA.equals(tag)) {
File[] externalMediaDirs = context.getExternalMediaDirs();
if
(externalMediaDirs.length > 0) {
target = externalMediaDirs[0];
if (target != null) {
strat.addRoot(name, buildPath(target, path));
return strat;
可以发现,这个方法主要是用来解析
XML
中的配置,获取到
<paths>
标签下配置的各种路径,并通过配置获取到具体的
File
对象并存储到最终的
PathStrategy
对象中。从这段代码中也可以得出结论:
PathStrategy
类中维护了
XML
中配置的各种路径,这一点从其默认的实现类
SimplePathStrategy
中就能看出来:
static class SimplePathStrategy implements PathStrategy {
private final HashMap<String, File> mRoots = new HashMap<String, File>();
@Override
public Uri getUriForFile(File file) {
String path;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
Map.Entry<String, File> mostSpecific = null;
for (Map.Entry<String, File> root : mRoots.entrySet()) {
final String rootPath = root.getValue().getPath();
if (path.startsWith(rootPath) && (mostSpecific == null || rootPath.length() > mostSpecific.getValue().getPath().length())) {
mostSpecific = root;
if (mostSpecific == null) {
throw new IllegalArgumentException("Failed to find configured root that contains " + path);
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/");
return new Uri.Builder().scheme("content").authority(mAuthority).encodedPath(path).build();
@Override
public File getFileForUri(Uri uri) {
String path = uri.getEncodedPath();
final int splitIndex = path.indexOf('/', 1);
final String tag = Uri.decode(path.substring(1, splitIndex));
path = Uri.decode(path.substring(splitIndex + 1));
final File root = mRoots.get(tag);
if (root == null) {
throw new IllegalArgumentException("Unable to find configured root for " + uri);
File file = new File(root, path);
try {
file = file.getCanonicalFile();
} catch (IOException e) {
throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
if (!file.getPath().startsWith(root.getPath())) {
throw new SecurityException("Resolved path jumped beyond configured root");
return file;
可见,这个类中通过一个
HashMap
的数据结构存储了各种文件夹的
File
对象。我们再重点看一下
getUriForFile()
方法,这个方法也是最开始的门面方法
FileProvider.getUriForFile()
中调用的方法。可以看到,在这个方法的最后,通过
Uri.Builder
的构建者方式构建了
Uri
对象,其格式就是前面Demo中打印出来的
Uri
的格式。
再看
getFileForUri()
方法,这个方法主要是对
Uri
字符串进行解析,从
HashMap
缓存中取出对应的路径文件夹,然后再根据具体的文件名获取到具体的文件并返回即可。