一.序
在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 API
,content:// URI
,临时访问权限
2.2 可能需要关注的点
Uri.parse
Uri.fromFile
file://
content://
Context.getFilesDir()
Environment.getExternalStorageDirectory()
getCacheDir()
intent.setDataAndType(为什么需要找这个,因为这个会携带uri进行传递,这个是重头戏)
关键字:intent.setDataAndType
三. 操作步骤
- 定义一个 FileProvider
- 指定共享目录
- 为文件生成有效的 Content URI
- 申请临时的读写权限
- 发送 Content URI 至其他的 App
3.1 定义一个 FileProvider
因为是ContentProvider的子类,所以也必须要在Manifest.xml中声明
...
...
android:name="android.support.v4.content.FileProvider"
的写法是固定的,不过如果你打算作为lib提供给别人可能要考虑冲突,可以继承这个类,然后不实现,以作区分。
grantUriPermissions:声明为true,你才能获取临时共享权限
3.2 指定共享目录
注意目录冲突问题
可以看出,这五种子元素基本涵盖内外存储空间所有目录路径,包含应用私有目录。同时,每个子元素都拥有 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 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 拍照裁剪全解析