• 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>
        <!-- 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 缓存中取出对应的路径文件夹,然后再根据具体的文件名获取到具体的文件并返回即可。

  • 手把手教你注册和使用ChatGPT
  • ChatGPT保姆级教程,一分钟学会使用ChatGPT!
  •