问题
通过startActivity去对外打开、分享数据时出现
android.os.FileUriExposedException: file:///storage/emulated/0/xxx exposed beyond app through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1960)
at android.net.Uri.checkFileUriExposed(Uri.java:2356)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9898)
at android.content.Intent.prepareToLeaveProcess(Intent.java:9850)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1612)
at android.app.Activity.startActivityForResult(Activity.java:4555)
at android.app.Activity.startActivityForResult(Activity.java:4513)
at android.app.Activity.startActivity(Activity.java:4874)
at android.app.Activity.startActivity(Activity.java:4842)
这种问题对于适配过Android 7调用系统安装器覆盖安装的开发者来说不陌生。
如安装类的场景
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
startActivity(intent);
文档打开的场景
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(file), "application/pdf");
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
原因分析
在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。
要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。也就是说,对于应用间共享文件这块,Android N中做了强制性要求
解决方法
方法一
如果对targetSdkVersion没有明确要求,可以将targetSdkVersion降低到24以下,如果targetSdkVersion 23,则不存在以上问题
方法二
使用FileProvider进行解决
FileProvider使用大概分为以下几个步骤:
- manifest中申明FileProvider
- res/xml中定义对外暴露的文件夹路径
- 生成content://类型的Uri
- 给Uri授予临时权限
- 使用Intent传递Uri
1. manifest中申明FileProvider:
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.demo.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/paths" />
</provider>
...
</application>
</manifest>
AndroidX第一行应变为
androidx.core.content.FileProvider
android:name:provider你可以使用v4包提供的FileProvider,或者自定义的,只需要在name申明就好了,一般使用系统的就足够了。
android:authorities:类似schema,命名空间之类,后面会用到。
android:exported:false表示我们的provider不需要对外开放。
android:grantUriPermissions:申明为true,你才能获取临时共享权限。
2. res/xml中定义对外暴露的文件夹路径:
新建filepaths.xml,文件名随便起,后面会引用到。
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external" path=“"/>
</paths>
paths里面的界面分别代表:
<files-path/>
代表context.getFileDir()
<cache-path/>
代表context.getCacheDir()
<external-path/>
代表Environment.getExternalStorageDirectory()
<external-files-path/>
代表context.getExternalFilesDirs()
<external-cache-path/>
代表context.getExternalCacheDirs()
path="/."
表示的是当前目录下的所有目录。
注意:如果你选择吧这些选项全部都加到资源文件中,那么这些条目的name
属性值要互不相同。
声明之后,代码中就可以使用所声明的当前文件夹以及其子文件夹。
3. 使用FileProvider
如安装代码:
Intent intent = new Intent(Intent.ACTION_VIEW);
File apkFile = new File(apkSavePath);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri uri = FileProvider.getUriForFile(getActivity(), getActivity().getPackageName() + ".fileprovider", apkFile);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
} else {
intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive");
}
startActivity(intent);