最近花时间梳理下常规的系统安装apk流程,主要分三大部分:三方应用发起apk安装、PackageInstaller中转apk安装,PakcageManagerService实现apk安装。那么这篇先从三方应用发起apk安装开始。
一、代码安装apk方案
< android 7.0
private void startInstall() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mAct.startActivity(intent);
}
= android 7.0
私有数据保护(使用file scheme会报如下类型错误)
2021-04-23 17:46:14.605 4133-4133/com.stan.installtest E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.stan.installtest, PID: 4133
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.stan.installtest/cache/sina_sports.apk exposed beyond app through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
官方说明:
https://developer.android.google.cn/reference/android/os/FileUriExposedException.html
从Android 7.0开始,不允许app把scheme为file的uri暴露给其他app,该报错即为对此的限制。scheme需要从file切换为content,通过ContentProvider来暴露私有数据给其他程序:
private void startInstallN() {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri apkUri = FileProvider.getUriForFile(mAct, Constants.AUTHORITY, new File(filePath));
intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
mAct.startActivity(intent);
}
AndroidMainfest.xml
res/xml/file_paths.xml
根据实际情况暴露对应apk获取路径
>= android 8.0
安装未知来源动态手动授权
private void startInstallO() {
boolean isGranted = mAct.getPackageManager().canRequestPackageInstalls();
if (isGranted) {
startInstallN();
return;
}
new AlertDialog.Builder(mAct)
.setCancelable(false)
.setTitle("安装应用需要打开未知来源权限,请去设置中开启权限")
.setPositiveButton("确定", (d, w) -> {
Intent intent = new Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES);
mAct.startActivityForResult(intent, UNKNOWN_CODE);
})
.show();
}
二、android.os.FileUriExposedException限制研究
Android不再允许在app中把file://Uri暴露给其他app,包括但不局限于通过Intent或ClipData 等方法。
原因在于使用file://Uri会有一些风险,比如:
- 文件是私有的,接收file://Uri的app无法访问该文件。
- 在Android6.0之后引入运行时权限,如果接收file://Uri的app没有申请READ_EXTERNAL_STORAGE权限,在读取文件时会引发崩溃。
因此,google提供了FileProvider,使用它可以生成content://Uri
来替代file://Uri
。
这里简单研究下限制策略,通过限制报错来反推:
2021-04-23 17:46:14.605 4133-4133/com.stan.installtest E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.stan.installtest, PID: 4133
android.os.FileUriExposedException: file:///storage/emulated/0/Android/data/com.stan.installtest/cache/sina_sports.apk exposed beyond app through Intent.getData()
at android.os.StrictMode.onFileUriExposed(StrictMode.java:1799)
at android.net.Uri.checkFileUriExposed(Uri.java:2346)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8933)
at android.content.Intent.prepareToLeaveProcess(Intent.java:8894)
at android.app.Instrumentation.execStartActivity(Instrumentation.java:1517)
从报错信息获取异常抛出的调用栈,得到核心检测方法:
frameworks/base/core/java/android/net/Uri.java
public void checkFileUriExposed(String location) {
if ("file".equals(getScheme())
&& (getPath() != null) && !getPath().startsWith("/system/")) {
StrictMode.onFileUriExposed(this, location);
}
}
frameworks/base/core/java/android/os/StrictMode.java
/** @hide */
public static void onFileUriExposed(Uri uri, String location) {
final String message = uri + " exposed beyond app through " + location;
if ((sVmPolicy.mask & PENALTY_DEATH_ON_FILE_URI_EXPOSURE) != 0) {
throw new FileUriExposedException(message);//异常抛出点
} else {
onVmPolicyViolation(new FileUriExposedViolation(message));
}
}
简单测试发现,这里有两个方式可以绕过file uri检查:
1)反射调用StrictMode的disableDeathOnFileUriExposure方法,禁用运行时检查
public static boolean disableDeathOnFileUriExposure() {
try {
Method m = StrictMode.class.getMethod("disableDeathOnFileUriExposure");
m.invoke(null);
} catch (Exception e) {
return false;
}
return true;
}
2)Uri的checkFileUriExposed 中有个判断条件是!getPath().startsWith("/system/“) 进入检查
那么我可以构造以/system/开头的路径来绕过:
String fpath = "/system/.." + filePath;
intent.setDataAndType(Uri.parse("file://" + filePath), "application/vnd.android.package-archive”);
至于说file和content的选择,和应用场景这就不铺开分析了。