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简介
FileProvider
是 ContentProvider
一个特殊的子类,用于通过创建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步骤:
- 在application标签下添加一个标签
- 设置android:name 属性为android.support.v4.content.FileProvider
- 基于app的包名来设置android:authorities属性,例如:包名为mydomain.com,那么授权路径为:com.mydomain.fileprovider
- 设置android:exported 属性为false;FileProvider不需要public
- 设置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文件中,以标签的形式指定文件目录.
比如下面的代码,表明你计划为 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:
- 为该文件创建一个File对象
- 将File对象传递给getUriForFile(),获取一个URI对象
- 将该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:
通过调用Intent
的 setData()
方法将此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]:
-
<external-cache-path name="name" path="path" />
该方式提供在应用的外部缓存区根目录的文件。它对应Context.getExternalCacheDir()
返回的路径。
ContextCompat.getExternalCacheDirs(MainActivity.this)[0]:
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.sadaharusong.fileprovider", apkFile);
//授予临时访问权限
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(contentUri,"application/vnd.android.package-archive");
//跳往安装界面
mContext.startActivityForResult(intent,INSTALL_APP);
其他涉及到本地文件读取的操作,例如图库,操作方式都一样,跟以前唯一的不同仅仅只是FileProvider的引入.