FileProvider的使用及原理解析

6,116 阅读8分钟

参考资料:

FileProviderContentProvider的一个子类,它通过创建一种content://格式的Uri来加强应用之间共享文件的安全性,这是传统的file:///格式的Uri实现不了的。

content://格式的Uri可以让我们为某个文件赋予让其他文件临时读写的权限,可以通过Intent#addFlags()方法添加权限,这些权限可以保留到目标ActivityService被销毁。

相比之下,使用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是否是公有的,一般情况下都设为falseandroid: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_PATHSandroid: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文件中,否则无法共享给其他应用,如上述代码中就是在别名为externalmy_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_PERMISSIONIntent.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缓存中取出对应的路径文件夹,然后再根据具体的文件名获取到具体的文件并返回即可。