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 Pre N
  • Android N:

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 拍照裁剪全解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容