FileProvider的原理和使用

为什么需要使用FileProvider ?

为了提高私有目录的安全性,防止应用信息的泄漏,从 Android 7.0 开始,应用私有目录的访问权限被做限制。具体表现为,开发人员不能够再简单地通过 file:// URI 访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。
同时,也是从 7.0 开始,Android SDK 中的 StrictMode 策略禁止开发人员在应用外部公开 file:// URI。具体表现为,当我们在应用中使用包含 file:// URI 的 Intent 离开自己的应用时,程序会发生故障。
开发中,如果我们在使用 file:// URI 时忽视了这两条规定,将导致用户在 7.0 及更高版本系统的设备中使用到相关功能时,出现 FileUriExposedException 异常,导致应用出现崩溃闪退问题。而这两个过程的替代解决方案便是使用 FileProvider。

FileProvider的兼容性错误
使用FileProvider的时候报告错误:Didn't find class "android.support.v4.content.FileProvider" on path:将v4包替换成androidx.core的FileProvider

<provider
    android:name="android.support.v4.content.FileProvider"
	...
</provider>

更改为:

<provider
	android:name="androidx.core.content.FileProvider"
	...
</provider>

关于更多的错误参见:FileProvider 不同版本变化和兼容

FileProvider的使用

1. 声明FileProvider
AndroidManifest.xmlapplication下面增加provider

<provider
	android:name="androidx.core.content.FileProvider"	
	android:authorities="app包名.fileProvider"
    android:grantUriPermissions="true"
    android:exported="false">
    <meta-data
    	android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

android:authorities需要是唯一的,使用唯一的android:authorities与xml指定的共享目录进行关联。一般使用app包名.fileProvider,当然这个名字也可以修改,但是必须唯一。也可以直接写成android:authorities="${applicationId}.fileProvider"

strat = sCache.get(authority);

2. 编写FileProvider的xml,指定共享目录
在res下面创建xml目录,然后新建file_paths.xml文件,内容如下(tag内容随自己定义):

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <!--代表的目录即为:Environment.getExternalStorageDirectory()/Android/data/包名/-->
    <external-path
        name="files_root"
        path="Android/data/com.example.qsbk/" />

    <!--代表的目录即为:Environment.getExternalStorageDirectory()-->
    <external-path
        name="external_storage_root"
        path="." />

    <!--代表的目录即为:Environment.getExternalStorageDirectory()/pics -->
    <external-path
        name="external"
        path="pics" />

</paths>

xml的内容格式,一句话总结如下:

<tag name="myname" path="mypath">

此时产生的共享目录如下:tag代表的目录+path指定的目录。保存在以name为key的hashmap中,所以上述模板会产生一条共享目录,内容如下:

private final HashMap<String, File> mRoots = new HashMap<String, File>();
mRoots[“myname”] = new File(tag,mypath)

之后调用getUriForFile生成共享Uri的时候,会遍历mRoots查找最佳的File目录,而name 属性则是指定的目录的一个别名。然后通过Uri.Builder生成 content:// uri。如果没有找到匹配的目录,则抛出异常IllegalArgumentException。所以至少得指定一个tag,tag可以是下面的元素之一:

  • <root-path>:设备根目录/
  • <files-path>:context.getFilesDir()的目录
  • <cache-path>:context.getCacheDir()的目录
  • <external-path>:Environment.getExternalStorageDirectory()的目录
  • <external-files-path>:ContextCompat.getExternalFilesDirs()下标为0的目录
  • <external-cache-path>:ContextCompat.getExternalCacheDirs()下标为0的目录
  • <external-media-path>:context.getExternalMediaDirs()下标为0的目录
    对于上述目录有疑问的,可以学习android 目录结构 和 文件存储。关于Tag是如何生成的共享目录列表mRoots可以参考FileProvider源码:
    PathStrategy parsePathStrategy(Context context, String authority);

3. 生成 Content URI
在 Android 7.0 出现之前,我们通常使用 Uri.fromFile() 方法生成一个 File URI。这里,我们需要使用 FileProvider 类提供的公有静态方法 getUriForFile 生成 Content URI。比如:

Uri contentUri = FileProvider.getUriForFile(this,
                BuildConfig.APPLICATION_ID + ".fileProvider", myFile);

需要传递三个参数,第二个参数就是在AndroidManifest.xml中声明的android:authorities。第三个参数是我们指定的共享文件,该文件必须在FileProvider的xml指定的共享目录下(或者子目录下)否则会抛出异常IllegalArgumentException

String filePath = Environment.getExternalStorageDirectory()+
				"Android/data/com.example.qsbk/imges/temp/1.jpg";
File outputFile = new File(filePath);
if (!outputFile.getParentFile().exists()) {
    outputFile.getParentFile().mkdir();
}
Uri contentUri = FileProvider.getUriForFile(this,
				BuildConfig.APPLICATION_ID + ".fileProvider", outputFile);

生成的 Content URI 是这样的:

content://com.example.qsbk.fileProvider/files_root/imges/temp/1.jpg

其中Uri:
com.example.qsbk.fileProvider:是provider元素的 authorities 属性值
files_root:是xml指定共享目录的,其中name=“files_root”(files_root指定目录和共享文件路径最大程度 匹配)
imges/temp/1.jpg:是替换别名之后剩余没有匹配的部分路径。

4. 授予 Content URI 访问权限
生成 Content URI 对象后,需要对其授权访问权限。授权方式有两种:
第一种方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他应用授权访问 URI 对象。三个参数分别表示授权访问 URI 对象的其他应用包名,授权访问的 Uri 对象,和授权类型。其中,授权类型为 Intent 类提供的读写类型常量:

  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_GRANT_WRITE_URI_PERMISSION

或者二者同时授权。这种形式的授权方式,有效期截止至发生设备重启或revokeUriPermission撤销授权。

第二种方式,配合 Intent 使用。通过 setData 方法向 intent 对象添加 Content URI。然后使用 setFlags 或者 addFlags 方法设置读写权限,可选常量值同上。这种形式的授权方式,权限有效期截止至其它应用所处的堆栈销毁,并且一旦授权给某一个组件后,该应用的其它组件拥有相同的访问权限。

5. 提供 Content URI 给其它应用

拥有授予权限的 Content URI 后,便可以通过 startActivity或者 setResult 方法启动其他应用并传递授权过的 Content URI 数据。当然,也有其他方式提供服务。

如果你需要一次性传递多个 URI 对象,可以使用 intent 对象提供的 setClipData方法,并且 setFlags方法设置的权限适用于所有 Content URIs。

最常用的应用场景,相机拍照或者读取相册。

源码分析:
FileProvider加载的时候会调用attachInfoattachInfo内部通过provider元素的 authorities 获取缓存中的PathStrategy如果缓存中没有则生成一个,然后添加到缓存。

@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
	super.attachInfo(context, info);
	// Sanity check our security
    if (info.exported) {
	    throw new SecurityException("Provider must not be exported");
    }
    if (!info.grantUriPermissions) {
    	throw new SecurityException("Provider must grant uri permissions");
    }

    mStrategy = getPathStrategy(context, info.authority);
}

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;
    }

生成PathStrategy的时候会解析Provider关联的xml文件,根据tag生成共享目录列表HashMap<String, File>

    private static PathStrategy parsePathStrategy(Context context, String authority)
            throws IOException, XmlPullParserException {
        ...
        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;
    }

最后使用getUriForFile生成uri

        @Override
        public Uri getUriForFile(File file) {
            String path;
            try {
            	//获取共享文件的规范化路径,不懂的可以参考
            	//https://blog.csdn.net/CAir2/article/details/106782930
                path = file.getCanonicalPath();
            } catch (IOException e) {
                throw new IllegalArgumentException("Failed to resolve canonical path for " + file);
            }

            // Find the most-specific root path
            //遍历xml指定的共享目录列表,获取最佳匹配的一项
            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);
            }
			
			//使用最匹配的一项,生成content格式的uri
            // 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();
        }

参考博客:https://blog.csdn.net/growing_tree/article/details/71190741

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章