为了提高私有文件的安全性,在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 。这里有两种解决方案:
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);
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的功能,因此你就不必再在代码中定义一个它的子类了.你可以直接在XML文件中声明一个FileProvider.
声明FileProvider步骤:
<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 需要使用自定义类的全路径类名
一个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>
标签必须包含一个或多个下列标签
注意
必须为每个需要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通过
标签与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>
为了与其他app通过content URI来分享文件,你的app需要生成一个content URI.方法如下:
分享方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
从getUriForFile()获取到content URI以后,通过以下任意一种方式授予访问权限
方式1:
调用Context.grantUriPermission(package, Uri, mode_flags)
为content URI 授权.使用指定的mode_flags
这里需指定授权的包名和mode_flags,权限分为
方式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.
为一个文件提供content URI给其他app有很多形式,其中一个常用的方式时接收其他app通过startActivityResult()
方法启动自己的app , 通过Intent来启动自己app中的一个Activity.
你可以立即返回一个content URI , 或者展示一个交互界面供用户选择一个文件 , 一旦用户选择了该文件 , 就将该文件的的content URI返回给请求者app . 无论哪种方式 , 最终都通过setResult()
方式将content URI返回给请求者.
将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类型
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类型:
Context.getFilesDir()
返回的路径,例如/data/data/com.crocutax.mytest/files
Context.getCacheDir()
返回的路径,例如/data/data/com.crocutax.mytest/cache
Environment.getExternalStorageDirectory()
返回的路径,例如/storage/emulated/0
Context.getExternalFilesDir(String type)
返回的路径。例如ContextCompat.getExternalFilesDirs(MainActivity.this,null)[0]:
Context.getExternalCacheDir()
返回的路径。ContextCompat.getExternalCacheDirs(MainActivity.this)[0]:
<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>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="sdcard_path" path="" />
paths>
//安装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的引入.