Android N 7.0 FileProvider 兼容适配 原理解析

一.序

在Android 7.0适配时,最常见,也是最重要的一点就是。当调用系统相机裁剪的时候,会出现Crash。查看Log可以很容易的发现是遇到了FileUriExposedException,这是因为当TargetSdkVersion升级到24的时候,file://在应用间传递将不再被允许。
关键字:应用间

二.探索FileProvider

2.1 简介

在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

引用自官网

在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。

关键字:离开您的应用StrictMode APIcontent:// URI临时访问权限

2.2 可能需要关注的点

Uri.parse 
Uri.fromFile 
file:// 
content:// 
Context.getFilesDir()
Environment.getExternalStorageDirectory()
getCacheDir()
intent.setDataAndType(为什么需要找这个,因为这个会携带uri进行传递,这个是重头戏)

关键字:intent.setDataAndType

三. 操作步骤

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

3.1 定义一个 FileProvider

因为是ContentProvider的子类,所以也必须要在Manifest.xml中声明

<application>
    ...
    <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>
    ...
</application>

android:name="android.support.v4.content.FileProvider"的写法是固定的,不过如果你打算作为lib提供给别人可能要考虑冲突,可以继承这个类,然后不实现,以作区分。

grantUriPermissions:声明为true,你才能获取临时共享权限

3.2 指定共享目录

注意目录冲突问题

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--        xml文件是唯一设置分享的目录 ,不能用代码设置

         1.<files-path>        getFilesDir()  /data/data//files目录
         2.<cache-path>        getCacheDir()  /data/data//cache目录
         3.<external-path>     Environment.getExternalStorageDirectory()
         SDCard/Android/data/你的应用的包名/files/ 目录
         4.<external-files-path>     Context#getExternalFilesDir(String) Context.getExternalFilesDir(null).
         5.<external-cache-path>      Context.getExternalCacheDir().
     -->
    <!--    path :代表设置的目录下一级目录 eg:<external-path path="images/"
                整个目录为Environment.getExternalStorageDirectory()+"/images/"
            name: 代表定义在Content中的字段 eg:name = "myimages" ,并且请求的内容的文件名为default_image.jpg
                则 返回一个URI   content://com.example.myapp.fileprovider/myimages/default_image.jpg
    -->
    <!--当path 为空时 5个全配置就可以解决-->
    <!--下载apk-->
    <external-path path="" name="sdcard_files" />
    <!--相机相册裁剪-->
    <external-files-path   path="file/" name="camera_has_sdcard"/>
    <files-path path=""     name="camera_no_sdcard"/>
</paths>

可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 name 和 path 两个属性。
path 属性用于指定当前子元素所代表目录下需要共享的子目录名称。注意:path 属性值不能使用具体的独立文件名,只能是目录名。path只能添加一个路径,如果需要共享多个则指定多个即可。
name 属性用于给 path 属性所指定的子目录名称取一个别名。后续生成 content:// URI 时,会使用这个别名代替真实目录名。这样做的目的,很显然是为了提高安全性。

3.3 生成有效的 Content URI

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    Uri uriForFile = FileProvider.getUriForFile(this, 
        "{applicationId(替换成包名)}.fileprovider", mCameraFile);
    intentFromCapture.putExtra(MediaStore.EXTRA_OUTPUT, uriForFile);
 }

最后生成的 Content URI 为
content://com.domain.example.provider/images/default_image.jpg.

目标文件会通过
context.getContentResolver().openFileDescriptor()得到一个ParcelFileDescriptor对象。再通过IOStream的方式操作这个文件。

3.4 申请临时的读写权限

生成 Content URI 对象后,需要对其授权访问权限。授权方式有两种: 第一种方式,使用 Context 提供的 grantUriPermission(package, Uri, mode_flags) 方法向其他应用授权访问 URI 对象。

FLAG_GRANT_READ_URI_PERMISSION
FLAG_GRANT_WRITE_URI_PERMISSION

或者二者同时授权。这种形式的授权方式,权限有效期截止至发生设备重启或者手动调用 revokeUriPermission() 方法撤销授权时。

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

3.5 发送URI

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      Uri uri = getUriForFile(context, file);
      intent.setDataAndType(uri, type);
      intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
      if (writeAble) {
        intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
      }
      context.grantUriPermission(context.getPackageName(), uri,
          Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
      intent.setDataAndType(Uri.fromFile(file), type);
    }
  public static Uri getUriForFile(Context context, File file) {
    if (context == null || file == null) {
      return null;
    }
    Uri fileUri;
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      fileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".provider", file);
    } else {
      fileUri = Uri.fromFile(file);
    }
    context.grantUriPermission(context.getPackageName(), fileUri,
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    context.grantUriPermission(context.getPackageName(), fileUri,
        Intent.FLAG_GRANT_READ_URI_PERMISSION);
    return fileUri;
  }

四. 变化

  • Android Pre-N:

《Android N 7.0 FileProvider 兼容适配 原理解析》 Android Pre N

  • Android N:

《Android N 7.0 FileProvider 兼容适配 原理解析》 Android N

为什么不允许直接传递file://

  • 最主要的原因就是如果文件的原始路径发送给目标App,那么目标APP就获得了这个文件的完整权限,(原文:If file path is sent to the target application (Camera app in this case), file will be fully accessed through the Camera app's process not the sender one.)。而这个文件的所有权应该是我们的APP而不是目标App。
  • 使用FileProvider,其实就是收回控制权,通过赋予相机程序临时的读写权限,掌握File文件的绝对控制权。
  • 这不是与ContentProvider的设计思想高度一致嘛。当我们在应用间共享数据的时候。应该提供的是接口。而不是把DB文件直接交给目标App,让他直接做增删改查。

思考题:

  • 那为什么应用间共享会抛出FileUriExposedException?应用内就可以直接使file://吗?究竟是什么原理?为什么上面关键字会提到StrictMode API
  • ParcelFileDescriptor是什么?怎么实现文件读写的?

参考文章:

file:// scheme is now not allowed to be attached with Intent on targetSdkVersion 24 (Android Nougat). And here is the solution.
Android7.0 完美适配——FileProvider 拍照裁剪全解析

    原文作者:Jamin_正宗红罐辣酱
    原文地址: https://www.jianshu.com/p/6192f04eca11
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞