美文网首页Android TipsAndroid开发Android技术知识
Android N 7.0 FileProvider 兼容适配

Android N 7.0 FileProvider 兼容适配

作者: Jamin_正宗红罐辣酱 | 来源:发表于2018-02-01 10:51 被阅读54次

    一.序

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

    相关文章

      网友评论

        本文标题:Android N 7.0 FileProvider 兼容适配

        本文链接:https://www.haomeiwen.com/subject/chxdzxtx.html