Android N兼容问题(FileProvider的使用)

背景

从Android N开始不再允许以file://的方式访问其他应用的私有目录文件或者让其他应用访问自己的私有目录文件。在Android N上如果继续采用file://的方式访问则会直接抛出异常导致app崩溃。目前如果工程的targetSDK小于24,暂时还不会遇到问题,一旦升级上24以上则会出现上述问题,所以我们需要做好相关的兼容处理。

关于FileProvider

ContentProvider做为四大组件之一,用作处理应用间的资源共享问题。而FileProvider也是做为ContentProvider的一个特殊子类,将file://Uri转换为content://Uri来实现不同App间的文件安全共享。

FileProvider的使用流程

  1. 定义一个 FileProvider
  2. 指定有效的文件
  3. 为文件生成有效的 Content URI
  4. 申请临时的读写权限
  5. 发送 Content URI 至其他的 App

1. 定义一个FileProvider

在AndroidManifest.xml文件中配置

<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

其中属性包括以下4个:

  • name:固定为android.support.v4.content.FileProvider
  • authorities:组成URI的一个固定部分,一般采用applicationId而不是直接包名硬编码,因为要求对于每个APP而言是唯一的。
  • exported:false表示FileProvider不公开
  • grantUriPermissions:true表示允许临时读写文件

2. 指定有效的文件

在res/xml目录下新建一个xml文件,用于指定应用需要共享的目录文件。例如:

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="images/"/>
    ...
</paths>

其中的节点属性:name表示共享文件的名字,path表示文件的路径。

而节点<external-path>外部存储空间根目录,等同于 Environment.getExternalStorageDirectory() 所获取的目录路径。

除此之外还有以下节点:

  • <files-path>:内部存储空间应用私有目录下的 files/ 目录,等同于 Context.getFilesDir() 所获取的目录路径;
  • <cache-path>:内部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getCacheDir() 所获取的目录路径;
  • <external-files-path>:外部存储空间应用私有目录下的 files/ 目录,等同于 Context.getExternalFilesDir(null) 所获取的目录路径;
  • <external-cache-path>:外部存储空间应用私有目录下的 cache/ 目录,等同于 Context.getExternalCacheDir()所获取的目录路径;

假定已经编写完成上述的xml文件,并命名为file_path.xml,我们还需要在manifest文件的<provider>完成如下配置:

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="${applicationId}.provider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

3. 为文件生成有效的 Content URI

以前我们是通过Uri.fromFile() 方法生成一个 File URI。现在我们需要通过FileProvider提供的共有静态方法 getUriForFile(Context context, String authority, File file) 生成 Content URI。

假定你想要共享一个图片文件,文件存放的位置为手机内部存储空间下的 images 文件夹,图片文件名字为 default_name.jpg ,那么生成 Content URI 方式如下:

File imagePath = new File(getContext().getFilesDir(), "images");
File newFile = new File(imagePath, "default_image.jpg");
Uri contentUri = getUriForFile(getContext(), "com.mydomain.provider", newFile);

最后生成的 Content URI 为

content://com.domain.example.provider/images/default_image.jpg

4. 申请临时的读写权限

生成URI对象后,需要对其授予访问的权限,并为我们提供了以下两个权限:

  • FLAG_GRANT_READ_URI_PERMISSION
  • FLAG_GRANT_WRITE_URI_PERMISSION

5. 发送 Content URI 至其他的 App

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

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

让FileProvider支持外置SD卡

Android官方文档中没有关于FileProvider对外置SD卡的支持的任何说明。

而我们通过FileProvider的源码进行研究,发现如下代码:

 private static PathStrategy parsePathStrategy(Context context, String authority)
        ...
        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 = buildPath(DEVICE_ROOT, path);
                } else if (TAG_FILES_PATH.equals(tag)) {
                    target = buildPath(context.getFilesDir(), path);
                } else if (TAG_CACHE_PATH.equals(tag)) {
                    target = buildPath(context.getCacheDir(), path);
                } else if (TAG_EXTERNAL.equals(tag)) {
                    target = buildPath(Environment.getExternalStorageDirectory(), path);
                }
                if (target != null) {
                    strat.addRoot(name, target);
                }
            }
        }
        return strat;
    }

从上述代码中可以看出FileProvider支持的path类型中有个TAG_ROOT_PATH,它对应的值为root-pathroot-path在Android官方文档中是没有提及的。而经过验证后,我们发现root-path代表/也就是Android设备的根目录,所以我们可以通过将path设置为root-path来解决FileProvider无法使用外置SD卡的问题。

版权声明:

本文标题:Android N兼容问题(FileProvider的使用)

作者:Rabtman

原始链接:https://rabtman.com/2016/10/11/how_to_use_fileprovider/

本文采用署名-非商业性使用-禁止演绎4.0进行许可。

非商业转载请保留以上信息。商业转载请联系作者本人。