一. 背景
Andriod N及以上版本中,一个应用提供自身文件给其它应用使用时,如果是file://
格式的URI
的话,会由StrictMode
触发FileUriExposedException
异常, 通常是长这样的:
05-21 17:12:07.438: E/AndroidRuntime(29373): FATAL EXCEPTION: main
05-21 17:12:07.438: E/AndroidRuntime(29373): Process: test.fileprovider, PID: 29373
05-21 17:12:07.438: E/AndroidRuntime(29373): android.os.FileUriExposedException: file:///storage/emulated/0/youbing.apk exposed beyond app through Intent.getData()
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.os.StrictMode.onFileUriExposed(StrictMode.java:1958)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.net.Uri.checkFileUriExposed(Uri.java:2348)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.content.Intent.prepareToLeaveProcess(Intent.java:9766)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.content.Intent.prepareToLeaveProcess(Intent.java:9720)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.app.Instrumentation.execStartActivity(Instrumentation.java:1614)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.app.Activity.startActivityForResult(Activity.java:4508)
05-21 17:12:07.438: E/AndroidRuntime(29373): at android.support.v4.app.BaseFragmentActivityApi16.startActivityForResult(BaseFrag
官方是建议我们使用Fileprovider
来解决这种问题,当然我们也可以在应用内通过添加一个不设防的VmPolicy
来规避。
二. FileProvider的方式
1. AndroidManifest中申明FileProvider
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.test.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
android:authorities="com.test.fileprovider": 这个名称自己定义
android:exported="false":表明这个provider不用对外开放
android:grantUriPermissions="true": 这个一定要为true, 否则无法获得临时权限
2. res/xml中定义需要对外暴露的文件夹路径
<paths>里边的元素必须是一下的一个或者多个, 对要分享出去的文件,一定要声明 正确的路径,否则会报如下错误:
05-22 10:43:52.491: E/AndroidRuntime(19432): FATAL EXCEPTION: main
05-22 10:43:52.491: E/AndroidRuntime(19432): Process: test.fileprovider, PID: 19432
05-22 10:43:52.491: E/AndroidRuntime(19432): java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/youbing.apk
05-22 10:43:52.491: E/AndroidRuntime(19432): at android.support.v4.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:712)
05-22 10:43:52.491: E/AndroidRuntime(19432): at android.support.v4.content.FileProvider.getUriForFile(FileProvider.java:401)
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="external_path" path="."/>
<external-files-path name="external_files_path" path="."/>
<external-cache-path name="external_cache_path" path="."/>
<files-path name="files_path" path="."/>
<cache-path name="cache_path" path="."/>
<root-path name="root_path" path="."/>
</paths>
name:
名称标志字符串,不可以同名!
path:
文件夹“相对路径”,完整路径取决于当前的标签类型。
标签 | 对应目录 |
---|---|
<files-path name="name" path="path" /> | /data/data/pkgname/files/path/ |
<external-files-path name="name" path="path" /> | /storage/emulated/0/Android/data/pkgname/files/path/ |
<external-cache-path name="name" path="path" /> | /storage/emulated/0/Android/data/pkgname/cache/path/ |
<files-path name="name" path="path" /> | /data/data/pkgname/files/path/ |
<cache-path name="name" path="path" /> | /data/data/pkgname/cache/path/ |
<root-path name="name" path="path" /> | /path/ |
3. getUriForFile生成content://类型的Uri
String str = "/youbing.apk";
String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + str;
File file = new File(fileName);
Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.test.fileprovider", file);
通过FileProvider 提供的接口 getUriForFile(context, string , file)
获取处理后的Uri
, 参数二 就是在AndroidManifest.xm
l中声明的 android:authorities="com.test.fileprovider"
的值
4. 给Uri授予临时权限,并使用Intent传递Uri
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivity(intent);
我这里的demo是从apk中尝试调起系统安装器安装 一个apk的场景,调用代码如下:
String str = "/youbing.apk";
String fileName = Environment.getExternalStorageDirectory().getAbsolutePath() + str;
File file = new File(fileName);
Uri uri = FileProvider.getUriForFile(MainActivity.this, "com.test.fileprovider", file);
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(uri,"application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivity(intent);
三. VmPolicy 方式
这个很简单,通常就是在 Application的 oncreate方法中添加两句代码:
StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
StrictMode.setVmPolicy(builder.build());
四. 遇到过的与FileProvider相关的问题
- 在写这个demo中遇到的,因为场景是调起系统安装器,按照上面两种方式处理后还是无法调起安装器,原因是我的 targetSdkVersion 是 26, 对应Android 8.0。因此需要在 AndroidManifest中声明权限,否则无法调起PackageInstaller.
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
- 王者荣耀自升级,安装时提示 安装包解析失败
具体场景是 :进入王者荣耀后,下载完升级包,会自动拉起PackageInstaller进行安装,然后就弹出 安装包解析失败对话框,无法完成升级
分析原因: 进入王者荣耀后,是横屏的,Android O上,下载完升级包拉起PackageInstaller的过程会依次进入 InstallStart - InstallStaging - PackageInstallerActivity 这几个activity,其中 InstallStart 和PackageInstallerActivity是指定强制竖屏,并且不随屏幕方向变化重走 activity 生命周期的
android:screenOrientation="portrait" 如果只指定强制竖屏,从横屏跳转过来时还是会走两次生命周期
android:configChanges="orientation|keyboardHidden|screenSize"
而InstallStaging重走了生命周期,onDestroy掉重新onCreate. 导致FileProvider授予的临时权限丢失,访问不到安装包文件URI,最终解析失败。
framework中临时权限的回收调用栈,从调用栈可以看到Uri临时访问权限确实随着目标activity的 destroy一起被移除掉了:
05-22 15:52:03.848: E/john(17382): at com.android.server.am.ActivityManagerService.removeUriPermissionIfNeededLocked(ActivityManagerService.java:10961)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.UriPermissionOwner.removeUriPermissionLocked(UriPermissionOwner.java:95)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.UriPermissionOwner.removeUriPermissionsLocked(UriPermissionOwner.java:69)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.UriPermissionOwner.removeUriPermissionsLocked(UriPermissionOwner.java:64)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.ActivityRecord.removeUriPermissionsLocked(ActivityRecord.java:1537)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.ActivityStack.removeActivityFromHistoryLocked(ActivityStack.java:4119)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.ActivityStack.activityDestroyedLocked(ActivityStack.java:4344)
05-22 15:52:03.848: E/john(17382): at com.android.server.am.ActivityManagerService.activityDestroyed(ActivityManagerService.java:9125)
解决办法: 当然就是在源码PackageInstaller的AndroidManifest.xml中给InstallStaging activity标签内添加
android:screenOrientation="portrait"
android:configChanges="orientation|keyboardHidden|screenSize"
防止由activity重建导致的权限丢失