参考资料:
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>
<!-- new File("/") -->
<root-path name="root" path="my_root/" />
<!-- context.getFilesDir() -->
<files-path name="internal_files" path="my_internal_files/" />
<!-- context.getCacheDir() -->
<cache-path name="internal_cache" path="my_internal_cache/" />
<!-- Environment.getExternalStorageDirectory() -->
<external-path name="external" path="my_external/" />
<!-- Context.getExternalFilesDirs(context, null) -->
<external-files-path name="external_files" path="my_external_files/" />
<!-- Context.getExternalCacheDirs(context) -->
<external-cache-path name="external_cache" path="my_external_cache/" />
<!-- context.getExternalMediaDirs() -->
<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
,代码如下:
// 在外部存储中创建my_external文件夹,保证其存在
val imgDir = File(Environment.getExternalStorageDirectory(), "my_external")
if (!imgDir.exists()) {
imgDir.mkdirs()
}
// 在my_external文件夹中创建tmp_camera_capture.jpg文件,保证其存在
val imgFile = File(imgDir, "tmp_camera_capture.jpg")
if (!imgFile.exists()) {
imgFile.createNewFile()
}
// 通过FileProvider.getUriForFile创建文件的Uri
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) {
// 此处的uri是一个全局变量
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);
}
// Find the most-specific root path
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);
}
// Start at first char of path under root
final String rootPath = mostSpecific.getValue().getPath();
if (rootPath.endsWith("/")) {
path = path.substring(rootPath.length());
} else {
path = path.substring(rootPath.length() + 1);
}
// Encode the tag and path separately
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
缓存中取出对应的路径文件夹,然后再根据具体的文件名获取到具体的文件并返回即可。