相信技术的力量

Android7.0适配之FileProvider的使用

本文包括4个部分:

  • FileProvider登场的背景
  • 尝试翻译官方文档 FileProvider (只是自己学习的过程中顺便翻译了,并未校验,因为懒的弄;如发现错误,麻烦帮忙指正;如果掉坑里了,后果自负-_-!)
  • FileProvider支持的path类型
  • FileProvider的使用示例

Android 7.0 文件权限的变化

为了提高私有文件的安全性,在targetSdk版本为N或者以后版本的app中,其私有目录将会限制访问。这可以防止私有文件元数据的泄露,比如文件大小或者是文件是否存在。但这给开发者带来了一些不利的影响:

  • 文件的所有者不能放宽文件权限,如果你使用MODE_WORLD_READABLE
    或者 MODE_WORLD_WRITEABLE操作文件,将会触发SecurityException
  • 当跨package域传递file://的URI时,接收者得到的将是一个无权访问的路径,因此,这将会触发FileUriExposedException。对于这类操作,可以使用ContentProvider, 但官方推荐的方式是使用FileProvider.
    在targetSdk为Android N之前的系统版本中,可以使用如下方法调用系统相机拍照并存入指定路径中
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri uri = Uri.fromFile(sdcardTempFile);
intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);

但是当你将targetSdk设置为Android N时 , 在执行到这段代码时app就crash了,crash的原因便是FileUriExposedException 。这里有两种解决方案:

方案1: ContentProvider

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
ContentValues contentValues = new ContentValues(1);
contentValues.put(MediaStore.Images.Media.DATA, sdcardTempFile.getAbsolutePath());
Uri uri = context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues);

方案2: FileProvider

FileProvider简介

FileProviderContentProvider 一个特殊的子类,用于通过创建content:// 类型的URI来替代file:// 类型的URI,从而为一个app提供更加安全的文件分享操作.

一个content URI允许你授予临时的读写权限,当你创建一个包含content URI的intent时,为了将这个content URI发送给一个客户端app,还可以调用Intent.setFlags()方法添加权限.这个Content类型的URI只要app存在活跃的activity就会一直有效,一旦退出app,该URI失效.

相比之下,File://类型的URI一旦提供了以后,任何app都可以使用该URI,并且在主动改变URI路径之前,这个URI一直有效,可以随时访问.这使得安全性大为降低.
由于Content URI提供的更高等级的文件安全机制,使得FileProvider成为Android安全架构的一个关键部分.

FileProvider主要包含以下5方面的知识点:

  • 定义一个FileProvider
  • 指定可访问的文件
  • 为一个文件创建一个Content URI
  • 为URI提供临时权限
  • 将Content URI提供给另外一个app

1.定义一个FileProvider

由于FileProvider默认提供了为文件创建content URI的功能,因此你就不必再在代码中定义一个它的子类了.你可以直接在XML文件中声明一个FileProvider.

声明FileProvider步骤:

  1. 在application标签下添加一个标签
  2. 设置android:name 属性为android.support.v4.content.FileProvider
  3. 基于app的包名来设置android:authorities属性,例如:包名为mydomain.com,那么授权路径为:com.mydomain.fileprovider
  4. 设置android:exported 属性为false;FileProvider不需要public
  5. 设置android:grantURIPermissions 属性为true,允许文件的临时访问
<manifest>
    ...
    <application>
        ...
        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.mydomain.fileprovider"
            android:exported="false"
            android:grantURIPermissions="true">
            ...
        </provider>
        ...
    </application>
</manifest>

如果想重写FileProvider中的方法,那么继承FileProvider类,并且在XML文件中的声明时,android:name 需要使用自定义类的全路径类名

2.指定可访问的文件

一个FileProvider只能为提前指定好的文件目录生成content URI.可以通过在xml文件中,以<paths>标签的形式指定文件目录.
比如下面的代码,表明你计划为 images/ 目录下的子文件请求content URI

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

<paths>标签必须包含一个或多个下列标签

  • <files-path name="name" path="path" />
    内部存储路径,与Context.getFilesDir()返回的路径一致

  • <cache-path name="name" path="path" />
    内部缓存路径,与Context.getExternalFilesDir() 返回的路径一致

  • <external-path name="name" path="path" />
    外置存储卡根目录,与Context.getExternalFilesDir()返回的路径一致

注意

  • name代表URI的路径,为了安全起见,隐藏了具体的目录位置 , 具体的目录位置由path字段指定
  • 所有的path指定的都是目录名,包含了旗下的子目录,而不是文件名.无法通过文件名来指定单个文件,也无法通过通配符的形式指定一系列子文件.

必须为每个需要content URI的路径在xml提供标签来指定,比如下面的代码就提供了两个目录

<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>

在资源目录下创建对应的xml文件,比如res/xml/file_paths.xml,在Manifest文件中将路径xml通过<meta-data>标签与FileProvider绑定起来.

<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.mydomain.fileprovider"
    android:exported="false"
    android:grantURIPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

3.为文件生成content URI

为了与其他app通过content URI来分享文件,你的app需要生成一个content URI.方法如下:

分享方app:

  1. 为该文件创建一个File对象
  2. 将File对象传递给getUriForFile(),获取一个URI对象
  3. 将该URI对象通过intent传递给其他的app

接收方app:
通过ContentResolver.openFileDescriptor获取一个ParcelFileDescriptor , 读取该文件

例如:
假设你的app需要提供给其他app一个FileProvider , authority授权名为com.mydomain.fileprovider , 为内部存储目录images/ 目录下的default_image.jpg 文件创建一个content URI.

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

代码结果:
getUriForFile()方法返回了一个contentUri , 路径内容为: content://com.mydomain.fileprovider/my_images/default_image.jpg

4.给URI提供临时权限

从getUriForFile()获取到content URI以后,通过以下任意一种方式授予访问权限

方式1:
调用Context.grantUriPermission(package, Uri, mode_flags)为content URI 授权.使用指定的mode_flags

这里需指定授权的包名和mode_flags,权限分为

  • FLAG_GRANT_READ_URI_PERMISSION 读
  • FLAG_GRANT_WRITE_URI_PERMISSION 写

可单选可多选
权限的有效期为:手动撤销授权revokeUriPermission() 或 重启设备.

方式2:
通过调用IntentsetData()方法将此content URI放入intent中
调用Intent.setFlags() ,选项为

  • FLAG_GRANT_READ_URI_PERMISSION 读
  • FLAG_GRANT_WRITE_URI_PERMISSION 写

可单选可多选
将此intent发送给其他app.一般情况下,会通过setResult()方法发送给其他intent

权限有效期 : 当接收到的activity处于活跃状态时持续有效 , 退出时自动失效,一个activity获取到得content URI权限,这个权限会延展至所属的整个app.

5.为其他app提供content URI

5.1其他app请求自己app

为一个文件提供content URI给其他app有很多形式,其中一个常用的方式时接收其他app通过startActivityResult()方法启动自己的app , 通过Intent来启动自己app中的一个Activity.
你可以立即返回一个content URI , 或者展示一个交互界面供用户选择一个文件 , 一旦用户选择了该文件 , 就将该文件的的content URI返回给请求者app . 无论哪种方式 , 最终都通过setResult()方式将content URI返回给请求者.

5.2自己app请求其他app

将content URI放入ClipData对象中,然后将ClipData对象添加进Intent中,再将Intent发送给一个app.

调用Intent.setClipData()来添加ClipData对象,可以放入1个或多个 . 每个ClipData对象都包含一个content URI

当通过Intent.setFlags()来设置临时访问权限时,这些权限会适用于所有的content URIs

注意

Intent.setClipData()方法只能在API 16(Android4.1)以上才能使用 , 如果为了确保版本的兼容性,那么只能每次通过intent发送一个content URI.将ACTION_SEND添加进action,通过setData()将content URI添加进data.

FileProvider支持的path类型

从FileProvider源码查看其中涉及的Path类型

private static final String TAG_ROOT_PATH = "root-path";
private static final String TAG_FILES_PATH = "files-path";
private static final String TAG_CACHE_PATH = "cache-path";
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";

从Android官方文档上可以看出FileProvider提供以下几种path类型:

  • <files-path path="" name="camera_photos" />

该方式提供在应用的内部存储区的文件/子目录的文件。它对应Context.getFilesDir()返回的路径,例如/data/data/com.crocutax.mytest/files

  • <cache-path name="name" path="path" />

该方式提供在应用的内部存储区的缓存子目录的文件。它对应Context.getCacheDir()返回的路径,例如/data/data/com.crocutax.mytest/cache

  • <external-path name="name" path="path" />

该方式提供在外部存储区域根目录下的文件。它对应Environment.getExternalStorageDirectory()返回的路径,例如/storage/emulated/0

  • <external-files-path name="name" path="path" />

该方式提供在应用的外部存储区根目录的下的文件。它对应Context.getExternalFilesDir(String type)返回的路径。例如

ContextCompat.getExternalFilesDirs(MainActivity.this,null)[0]:  
//log输出: /storage/emulated/0/Android/data/com.crocutax.mytest/files
  • <external-cache-path name="name" path="path" />

该方式提供在应用的外部缓存区根目录的文件。它对应Context.getExternalCacheDir()返回的路径。

ContextCompat.getExternalCacheDirs(MainActivity.this)[0]: 
//log输出: /storage/emulated/0/Android/data/com.crocutax.mytest/cache

FileProvider的使用示例

1.在Manifest文件中定义FileProvider

<provider
     android:name="android.support.v4.content.FileProvider"
     android:authorities="com.touchmedia.daolan.fileprovider"
     android:exported="false"
     android:grantUriPermissions="true">
     <meta-data
         android:name="android.support.FILE_PROVIDER_PATHS"
         android:resource="@xml/file_paths" />
 </provider>

2.res/xml/file_paths中指定共享目录

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

3.通过FileProvider获取ContentUri

//安装app
...
//通过FileProvider获取contentUri
Uri contentUri = FileProvider.getUriForFile(mContext, "com.touchmedia.daolan.fileprovider", apkFile);
//授予临时访问权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(contentUri,"application/vnd.android.package-archive");
//跳往安装界面
mContext.startActivityForResult(intent,INSTALL_APP);

其他涉及到本地文件读取的操作,例如图库,操作方式都一样,跟以前唯一的不同仅仅只是FileProvider的引入.

另外,可参考如下链接:

CommonsWare FileProvider专题
FileProvider无法获取外置SD卡问题解决方案
Android7.0适配心得

⬆️