应用安装(一) – 三方应用发起apk系统安装

时间:2021-7-5 作者:qvyue

最近花时间梳理下常规的系统安装apk流程,主要分三大部分:三方应用发起apk安装、PackageInstaller中转apk安装,PakcageManagerService实现apk安装。那么这篇先从三方应用发起apk安装开始。

一、代码安装apk方案

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的选择,和应用场景这就不铺开分析了。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。