应用间经常需要将自己的私有文件共享给其他的app,如某应用想要共享图库的图片用来编辑用户的头像,或者文件管理app允许用户在不同目录下复制粘贴文件等等,但为了保护私有文件的安全性,在targetSdk版本为N或者以后版本的app中,应用的私有目录被限制访问。。。
面向Android7.0的应用,Android框架执行的StrictMode API政策禁止在您的应用外部公开file://URI。如果一项包含文件URI的intent离开您的应用,则您的应用会停止运行,抛出FileUriExposedException
12-09 01:44:45.284 E/AndroidRuntime(17911): FATAL EXCEPTION: main
12-09 01:44:45.284 E/AndroidRuntime(17911): Process: com.example.fileproviderdemo, PID: 17911
12-09 01:44:45.284 E/AndroidRuntime(17911): android.os.FileUriExposedException: file:///data/user/0/com.example.fileproviderdemo/files/image exposed beyond app through ClipData.Item.getUri()
12-09 01:44:45.284 E/AndroidRuntime(17911): at android.os.StrictMode.onFileUriExposed(StrictMode.java:1958)
...
12-09 01:44:45.290 W/ActivityManager( 1148): Force finishing activity com.example.fileproviderdemo/.MainActivity
如果要在文件间共享文件,唯一安全的方式就是要发送一项content://URI,并授予URI临时访问权限。这个方式之所以安全,是因为只适用于收到这个URI的应用,且会自动过期。进行此授权最简单的方式就是使用FileProvider类
FileProvider是ContentProvider的一个特定的子类,通过创建content://类型的uri来替代file://类型的URI,从而为一个app提供了更加安全的文件分享操作。
接下来概述下如何使用FileProvider
FileProvider默认提供了为文件创建content Uri的功能,因此就不必再在代码中定义一个她的子类了。
FileProvider也是Android四大基本组件之一,直接在manifest中声明一个FileProvider,如下
"http://schemas.android.com/apk/res/android"
package="com.example.fileproviderdemo">
...
"com.example.filepandroid:authoritiesroviderdemo.fileprovider"//content uri
android:name="android.support.v4.content.FileProvider"
android:grantUriPermissions="true"//设置为true,才可授予临时权限
android:exported="false">//不需要暴露给外部,设置为false即可
"android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths"/>//共享的文件目录配置,下一小结具体介绍
...
FileProvider只会为提前指定好的文件目录生成content URI,通过在xml文件中,以元素指定的文件目录,如下面可为files/images目录下的文件生成content URI:
<paths>
<files-path name="photo" path="images/"/>
paths>
name=”photo” (A URI path segment)为了加强安全性,name定义的值隐藏了你真正要分享的子目录的名字,子目录的名字由path属性定义
path=”images” (The subdirectory you’re sharing你要分享的子目录)
name定义的值是content uri中的路径名,path定义的值是真正子目录的名字
注意:path定义的是一个子目录,而不是一个或多个私密文件,你不能通过文件名,也不能通过文件的子集(用通配符)共享一个文件
paths元素中可包括一个或多个子元素
<paths>
<files-path name="name" path="path"/> // files/文件子目录,可通过Context#getFilesDir()获取
<cache-path name="name" path="path" />// cache/文件子目录,可通过Context#getCacheDir()获取
<external-path name="name" path="path" />// Environment.getExternalStorageDirectory()
<external-files-path name="name" path="path" />// Context#getExternalFilesDir(String)
<external-cache-path name="name" path="path" />// Context#getExternalCacheDir()
paths>
调用FileProvider#getUriForFile()生成文件的Content Uri
File filePath = new File(getFilesDir(), "images");
File file = new File(filePath, "myself");
Uri contentUri = FileProvider.getUriForFile(this, "com.example.fileproviderdemo.fileprovider", file);
上述例子对应的file 文件目录:/data/user/0/com.example.fileproviderdemo/files/images/myself
FileProvider生成的Content Uri是content://com.example.fileproviderdemo.fileprovider/photo/myself
(content://android:authorities/name/fileName)
有两种为Content Uri授予临时权限的方式:
方法1: 调用Context.grantUriPermission(package, Uri, mode_flags) 这个方式是只会授权给指定包名的app,权限的有效期:手动调用 Context#revokeUriPermission(Uri uri, int modeFlags)撤销授权,或者重启手机列表内容
//功能,对目标文件进行裁剪,裁剪完并复制到输出文件
//inputUri是要裁剪的目标文件,outUri是裁剪后的输出文件
//Intent中有要传输两个Uri,必须用grantUriPermission方法,方法2只适用于一个uri的情况
Intent intent = new Intent(ACTION_CROP);
intent.setDataAndType(inputUri, "image/");
intent.putExtra(MediaStore.EXTRA_OUTPUT, outUri);
List infoList = getPackageManager().queryIntentActivities(intent, 0);
for (ResolveInfo resolveInfo: infoList) {
final String packageName = resolveInfo.activityInfo.packageName;
grantUriPermission(packageName, inputUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
grantUriPermission(packageName, outUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
startActivityForResult(intent, CODE_REQUEST_CROP);
方法2: 调用Intent#setFlags (int flags)或者Intent#addFlags (int flags),权限的有效期:当接受到的activity处于活跃状态时持续有效,退出则自动失效,一个activity获取到Content Uri的临时权限,这个权限会延展至这个应用的其他组件
//功能:拍照,并将照片复制到输出文件中
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, outUri);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(intent, CODE_REQUEST_TAKE_PHOTO);
方法1和方法2 中提到的flag均是Intent#FLAG_GRANT_READ_URI_PERMISSION 或Intent# FLAG_GRANT_WRITE_URI_PERMISSION
将Content Uri分享给其他应用,除了上个步骤中的例子通过setData的方案,也可以调用Intent#setClipData() ,只不过这个方法是Android sdk 16才开始支持的
附上测试demo: https://pan.baidu.com/s/1gfMXILH
参考文档:https://developer.android.com/reference/android/support/v4/content/FileProvider.html
https://developer.android.com/about/versions/nougat/android-7.0-changes.html
https://developer.android.com/training/secure-file-sharing/index.html