Android N权限机制的一点小变化

/ 0评 / 0

前言

Android 从 N(SDK >= 24) 开始不允许以 file:// 的方式通过 Intent 在两个 App 之间分享文件,取而代之的是通过 FileProvider 生成 content://Uri 。如果在 Android N 以上的版本继续使用 file:// 的方式分享文件,则系统会直接抛出FileUriExposedException 异常,导致 App 出现 Crash 。

解决方案

首先与Android6.0的动态权限类似,如果我们的targetSdkVersion<24,那么我们使用file://在app之间分享文件是一切ok的,但是这样处理起来虽然能偷懒,但是只是将坑留到未来的某一天而已,所以此种方案不推荐。

Google官方推荐使用FileProvider来将file://转换为content://,官方文档可以点这里

FileProvider的使用

FileProvider本质上是一个ContentProvider,只不过Google帮我们实现好了,所以我们可以直接拿过来使用。

1、首先需要在AndroidManifest.xml中注册此FileProvider。

<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/filepaths"/>
</provider>

android:name 为android.support.v4.content.FileProvider,我们也可以自己继承FileProvider避免冲突,具体查看本文中【FileProvider冲突】的解决;

android:authorities 为applicationId+.provider(不要使用硬编码)

android:exported 为 false ,表示 FileProvider 不是公开的;

android:grantUriPermissions 为 true 表示允许临时读写文件。

meta-data中配置FileProvider的配置文件为/xml/filepaths.xml,在里面进行配置即可。

需要注意的是:android:authorities必须为唯一的,这是系统对于ContentProvider的要求。假如 FileProvider 用在 SDK 中,多个 App 都在调用同一个 SDK,而 SDK 中的 android:authorities 为硬编码,那么 App 之间的 authorities 就会出现冲突,会报 Install shows error in console: INSTALL FAILED CONFLICTING PROVIDER 的错误

为防止出现上面的情况,我们可以使用applicationId代替包名。相应的AndroidManifest.xml需要修改为如下形式。

 android:authorities="${applicationId}.provider"

applicationId定义在\app\build.gradle中,默认为包名,这样如果 FileProvider 用在 SDK 中,那么每一个使用SDK的应用包名肯定不一致,就不会出现冲突了。相应参考链接点这里。ApplicationId 与 PackageName 的区别点击这里

2、然后在/res/xml/下新建一个filepaths.xml文件进行配置。

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="my_images" path="images/"/>
    <files-path name="my_docs" path="docs/"/>
</paths>

根标签为paths,子标签有如下几个选项。分别代表几个不同的根目录

files-path:指定根目录为Context.getFilesDir()

cache-path:指定根目录为getCacheDir()

external-files-path:指定根目录为Context#getExternalFilesDir(String) Context.getExternalFilesDir(null).

external-cache-path:指定根目录为Context.getExternalCacheDir()

external-path:指定根目录为Environment.getExternalStorageDirectory()

上面几个函数的返回值试着调用打印出来,就很清楚根目录在哪里了,或者可以看看以前的博客,点这里

子标签有两个属性可以设置分别为name以及path,name表示名字,会被添加到content://的后面,path为相对于根目录的子路径,可以为空

举个栗子。

<files-path name="my_images" path="images/"/>对应目录为/data/data/应用包名/files/images/

3、最后我们在代码中将file://转换为content://

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

最终转换结果为:content://com.mydomain.fileprovider/my_images/default_image.jpg。

然后在需要传递的位置加一下版本判断即可,Android N则使用FileProvider,其他版本则使用file://,记得要赋予url临时读写权限,没有权限也是无法读写指定目录的。

public void onOpenFile(View view) {
    File newTextFile = new File(getExternalCacheDir(), "a.txt");
    Intent intent = new Intent();
    intent.setAction(android.content.Intent.ACTION_VIEW);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        Uri uriForFile = FileProvider.getUriForFile(this, getPackageName() + ".provider", newTextFile);
        intent.setDataAndType(uriForFile, "text/plain");
        //授予此URL临时读写权限
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    } else {
        intent.setDataAndType(Uri.fromFile(newTextFile), "text/plain");
    }
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
}

进过上面的配置,我们就可以在Android N上面进行适配了。步骤还是很简单的。

FileProvider冲突

有时候我们的主项目定义了FileProvider,然后我们的lib中也定义了这个,当我们运行的时候,由于AndroidManifest.xml合并的问题,会造成冲突,解决方法就是定义一个类,继承FileProvider,提供一个空实现,然后将AndroidManifest.xml中的FileProvider的name标签进行修改即可

FileProvider兼容问题

在部分机型上面可能导致如下异常

Failed to find configured root that contains /storage/850F-18F9/Android/data/com.pmpd.dmap.lenovo/cache/newVersion_2.2.2.apk

出现原因,将文件存储在外置SD卡,不过外置SD卡可能不符合某种规范(猜测可能是可插拔的SD卡/storage/850F-18F9/Android),解决办法

1、方法一:将外部存储目录上面的文件存储到内置SD卡

2、方法二:使用隐藏的root-path标签(未验证)

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <root-path path="" name="camera_photos" />
    </paths>
</resources>

Demo地址:Github

参考链接:知乎专栏官方文档

发表评论

您的电子邮箱地址不会被公开。