# 起因
自动更新时,APP 覆盖安装失效了,由于先前测试调试都是手动覆盖安装,因此未发现问题
## 错误定位
通过调试发现
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=android.intent.action.VIEW dat=file:///storage/emulated/0/CESPIB/ces_pib.apk typ=application/vnd.android.package-archive flg=0x10000000 } | |
at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2113) | |
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1739) | |
at android.app.Activity.startActivityForResult(Activity.java:5343) | |
at android.support.v4.app.k.startActivityForResult(BaseFragmentActivityJB.java:50) | |
at android.support.v4.app.p.startActivityForResult(FragmentActivity.java:79) | |
at android.app.Activity.startActivityForResult(Activity.java:5284) | |
at android.support.v4.app.p.startActivityForResult(FragmentActivity.java:859) | |
at android.app.Activity.startActivity(Activity.java:5714) | |
at android.app.Activity.startActivity(Activity.java:5682) | |
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment.installApk(DownloadApkDialogFragment.java:166) | |
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment.access$300(DownloadApkDialogFragment.java:33) | |
at com.wuhanins.ces_pib.extra.util.DownloadApkDialogFragment$2.handleMessage(DownloadApkDialogFragment.java:150) | |
at android.os.Handler.dispatchMessage(Handler.java:107) | |
at android.os.Looper.loop(Looper.java:213) | |
at android.app.ActivityThread.main(ActivityThread.java:8147) | |
at java.lang.reflect.Method.invoke(Native Method) | |
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) | |
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100) |
具体错误出现在将文件转换为 Uri 跳转到安装 apk 的 intent 时出错,想了想,应该是 7.0 以后限制了 APP 对于文件 uri 的使用权限。果然,查看官网后发现了端倪:
# 在应用间共享文件
对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。要在应用间共享文件,您应发送一项 content:// URI,并授予 URI 临时访问权限。进行此授权的最简单方式是使用 FileProvider 类。如需了解有关权限和共享文件的详细信息,请参阅共享文件。
大意就是说文件的访问权限提高了,不能直接使用 "file://" 的方式来共享文件了,应该使用 "content://" URI 的方式来共享文件,并使用 FileProvider 类来授权。
# 处理方案
一般情况下我们安装 apk 会这样操作
private void installApk(String filePath) { | |
Intent intent = new Intent(Intent.ACTION_VIEW); | |
intent.setDataAndType(Uri.fromFile(new File(filePath)), "application/vnd.android.package-archive"); | |
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
mContext.startActivity(intent); | |
} |
但 7.0 以上要求使用 FileProvider 来授权访问文件
根据官方的指引:https://developer.android.google.cn/reference/android/support/v4/content/FileProvider ,大概需要以下几个步骤:
- 定义 FileProvider
- 指定可用文件
- 生成文件的 URI
- 授予 URI 临时权限
- 向另一个应用程序提供内容 URI
##### 下面来一一介绍一下这几个步骤:
- 定义 FileProvider
在 AndroidManifest.xml 文件中注册 provider
<provider | |
android:name="android.support.v4.content.FileProvider" | |
android:authorities="com.wuhanins.ces_pib.provider" | |
android:exported="false" | |
android:grantUriPermissions="true"> | |
<!-- 元数据 --> | |
<meta-data | |
android:name="android.support.FILE_PROVIDER_PATHS" | |
android:resource="@xml/update_apk_paths" /> | |
</provider> |
解释一下几个参数的含义:
android:name
文件提供者的类名,固定为 "android.support.v4.content.FileProvider",如果你很牛逼也可以自己写一个类并继承 "android.support.v4.content.FileProvider",然后实现一些扩展的功能。
android:authorities
权限的名字,用于标识 provider 提供的内容,可以有多个名字,各名字之间用 “;” 隔开。为了不和其它名字冲突一般使用域名的形式来描述。在实际使用过程中,我们一般会用项目的包名替换,例如
android:authorities="${applicationId}.provider" |
直接通过引用的方式添加 authorities
android:exported
内容提供者是否可供其他应用程序使用,在这里不需要,所以填 false
android:grantUriPermissions
是否授权给那些本来无权限访问的人临时访问内容提供者提供的内容,这里填 true,不然就没法访问到这个文件了。
- 指定可用的文件
为了指定需要访问的文件,需要在一个 xml 文件中指定被访问文件的存储路径。
在 res 目录下新建一个 xml 文件夹,然后新建一个文件:update_apk_paths.xml(文件名自己随意起), 内容如下:
<?xml version="1.0" encoding="utf-8"?> | |
<resources> | |
<paths> | |
<!-- | |
files-path: 该方式提供在应用的内部存储区的文件 / 子目录的文件。 | |
它对应 Context.getFilesDir 返回的路径:eg:”/data/data/com.jph.simple/files”。 | |
cache-path: 该方式提供在应用的内部存储区的缓存子目录的文件。 | |
它对应 getCacheDir 返回的路径:eg:“/data/data/com.jph.simple/cache”; | |
external-path: 该方式提供在外部存储区域根目录下的文件。 | |
它对应 Environment.getExternalStorageDirectory 返回的路径: | |
external-cache-path: 该方式提供在应用的外部存储区根目录的下的文件。 | |
它对应 Context#getExternalFilesDir (String) Context.getExternalFilesDir (null) | |
返回的路径。eg:”/storage/emulated/0/Android/data/com.jph.simple/files” | |
--> | |
<!-- name=“update” | |
相当于下面的 path 的别名,为了把真实的路径隐藏起来,这样就只能看到别名,如果按照这个别名路径去找文件的话肯定是找不到的。这个别名自己随便取,我把它叫做 “update” | |
path="" | |
代表你要分享的真实的子目录名,空字符串代表根目录,注意该值必须是一个子目录,不能是文件名 --> | |
<external-path | |
name="update" | |
path="CESPIB" /> | |
</paths> | |
</resources> |
综合来讲,以上配置表明:我要分享一个目录供其它人访问,这个目录就是内部存储区的缓存目录的根目录,即 getCacheDir () 的返回值。所有根目录及其子目录下的文件都可以被访问,同时我为这缓存目录取了一个别名叫 “update”, 以混淆视听。
当你要将自己的目录分享给其他应用使用时就需要下面的操作了,将上面的 update_apk_paths.xml 文件链接到 AndroidManifest.xml 中定义的 provider 中,也就是定义中的 “元数据” 的内容
<!-- 元数据 --> | |
<meta-data | |
android:name="android.support.FILE_PROVIDER_PATHS" | |
android:resource="@xml/update_apk_paths" /> |
android:name
代表资源的类型,此处为固定值 "android.support.FILE_PROVIDER_PATHS"
android:resource
代表资源文件,即 update_apk_paths.xml,但是不要后缀名
- 生成文件的 URI
用以下方式生成文件的 Uri:
Uri apkUri = FileProvider.getUriForFile(mContext, "com.wuhanins.ces_pib.provider", apkFile); |
其中,第二个参数 "com.wuhanins.ces_pib.provider" 是在 AndroidManifest.xml 文件中声明的 provider 中 android:authorities 元素的值,第三个参数 apkFile 就是下载下来的保存在缓存目录下的 apk 文件,假如你是用的是下面的方式声明的 authorities
android:authorities="${applicationId}.provider" |
那么你调用的时候最好是
Uri apkUri = FileProvider.getUriForFile(mContext, mContext.getApplicationInfo().packageName+".apk.provider", apkFile);
通过系统 API 获取当前包名,避免拼写出错
4. 授予 URI 临时权限
授权有很多种方式:这里只说一种,就是通过 Intent addFlags () 方法,如:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
- 向另一个应用程序提供内容 URI
用 startActivity (intent) 启动一个应用就可以了,被启动的应用就有权限访问你提供的文件了,但要注意必须添加这句:
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); |
# 最后
安装 apk 的方法就改造成了下面这样了
private void installApk(String filePath) { | |
File apkFile = new File(filePath); | |
Intent intent = new Intent(Intent.ACTION_VIEW); | |
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | |
// 7.0 以上 | |
Uri apkUri = FileProvider.getUriForFile(mContext, "com.wuhanins.ces_pib.provider", apkFile); | |
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); | |
intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); | |
} else { | |
// 7.0 以下 | |
Uri uri = Uri.fromFile(apkFile); | |
intent.setDataAndType(uri, "application/vnd.android.package-archive"); | |
} | |
mContext.startActivity(intent); | |
} |
自动更新失败的问题完美解决,其他例如调用系统相机,调用系统相册等等都可以类推了,但是切记一定要添加上 Flag
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);