Jimmy Chen

A Programmer

(原创)基于Android 8.1之PackageInstaller源码分析(一)

前言

  前面两篇文章简单讲述了PackageManagerService的启动过程,和APK安装的内容一直都没有涉及到。APK可以通过多种方式安装,例如通过adb install安装、系统开机会默认安装系统应用、将apk放到手机通过PackageInstaller交互安装等等。安装APK的方式很多,但是这里选择用户使用最多的安装方式进行讲解,即通过PackageInstaller交互式安装。PackageInstaller是Android系统的默认程序,源码路劲为packages/app/PackageInstaller,同样我们的代码基于Android 8.1

PackageInstaller分析起点

  在Android 7.0以前,我们可以通过下面的方式来调用PackageInstaller进行程序的安装

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setDataAndType(Uri.parse("file://xxxx.apk"),"application/vnd.android.package-archive");
context.startActivity(intent);

  而在Android 7.0及以上版本,上面的代码会出现FileUriExposedException的异常,导致整个的原因是因为Android 7.0中有如下行为变更

For apps targeting Android 7.0, the Android framework enforces the StrictMode API policy that prohibits exposing file:// URIs outside your app. If an intent containing a file URI leaves your app, the app fails with a FileUriExposedException exception.

翻译过来的意思大概就是:对于面向 Android 7.0 的应用,Android 框架执行的 StrictMode API 政策禁止在您的应用外部公开 file:// URI。如果一项包含文件 URI 的 intent 离开您的应用,则应用出现故障,并出现 FileUriExposedException 异常。

所以在Android 7.0及以上的版本普遍会使用如下代码调用PackageInstaller进行APK的安装:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (Build.VERSION.SDK_INT >= 24) {
    //参数1上下文;参数2 Provider主机地址 authorities 和配置文件中保持一致 ;参数3共享的文件
    Uri apkUri = FileProvider.getUriForFile(paramContext, "com.xxx.xxx.xxx", file);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setDataAndType(apkUri, "application/vnd.android.package-archive");
} else {
    intent.setDataAndType(Uri.fromFile(file), "application/vnd.android.package-archive");
}

总结一下这两段代码的共同之处就是:1)设置Intent的action为ACTION_VIEW;2)设置Intent TYPE为application/vnd.android.package-archive; 3)隐式启动activity

接下来我们查看PackageInstaller的AndroidManifest.xml看是否有Activity能够对得上上面的条件

<activity android:name=".InstallStart"
        android:exported="true"
        android:excludeFromRecents="true">
    <intent-filter android:priority="1">

        <action android:name="android.intent.action.VIEW" />
        <action android:name="android.intent.action.INSTALL_PACKAGE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="file" />
        <data android:scheme="content" />
        <data android:mimeType="application/vnd.android.package-archive" />

    </intent-filter>
    <intent-filter android:priority="1">
        <action android:name="android.intent.action.INSTALL_PACKAGE" />
        <category android:name="android.intent.category.DEFAULT" />
        <data android:scheme="file" />
        <data android:scheme="package" />
        <data android:scheme="content" />
    </intent-filter>
    <intent-filter android:priority="1">
        <action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

如上是PackageInstaller的AndroidManifest.xml的部分内容,其中红色部分真好匹配Action和mimeType。

Installstart.java

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // 获取PackageManager
    mIPackageManager = AppGlobals.getPackageManager();
    Intent intent = getIntent();
    String callingPackage = getCallingPackage();

    // 获取sessionID,在前面启动Intent前并没有设置sessionID这里跳过
    int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);
    if (callingPackage == null && sessionId != -1) {
        PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
        PackageInstaller.SessionInfo sessionInfo = packageInstaller.getSessionInfo(sessionId);
        callingPackage = (sessionInfo != null) ? sessionInfo.getInstallerPackageName() : null;
    }

    // 获取调用PackageInstaller的应用的信息
    final ApplicationInfo sourceInfo = getSourceInfo(callingPackage);
    final int originatingUid = getOriginatingUid(sourceInfo);
    boolean isTrustedSource = false;
    // 判断调用PackageInstaller的应用是否包含PRIVATE_FLAG_PRIVILEGED
    // PRIVATE_FLAG_PRIVILEGED在PKMS启动的时候有看到过,当时是在添加一些Share User的时候设置
    // 这里明显不是
    if (sourceInfo != null
            && (sourceInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
        isTrustedSource = intent.getBooleanExtra(Intent.EXTRA_NOT_UNKNOWN_SOURCE, false);
    }

    // 判断调用PackageInstaller的应用是否有申明REQUEST_INSTALL_PACKAGES权限
    if (!isTrustedSource && originatingUid != PackageInstaller.SessionParams.UID_UNKNOWN) {
        final int targetSdkVersion = getMaxTargetSdkVersionForUid(originatingUid);
        if (targetSdkVersion < 0) {
            Log.w(LOG_TAG, "Cannot get target sdk version for uid " + originatingUid);
            // Invalid originating uid supplied. Abort install.
            mAbortInstall = true;
        } else if (targetSdkVersion >= Build.VERSION_CODES.O && !declaresAppOpPermission(
                originatingUid, Manifest.permission.REQUEST_INSTALL_PACKAGES)) {
            Log.e(LOG_TAG, "Requesting uid " + originatingUid + " needs to declare permission "
                    + Manifest.permission.REQUEST_INSTALL_PACKAGES);
            mAbortInstall = true;
        }
    }
    if (mAbortInstall) {
        setResult(RESULT_CANCELED);
        finish();
        return;
    }

    Intent nextActivity = new Intent(intent);
    nextActivity.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);

    // The the installation source as the nextActivity thinks this activity is the source, hence
    // set the originating UID and sourceInfo explicitly
    nextActivity.putExtra(PackageInstallerActivity.EXTRA_CALLING_PACKAGE, callingPackage);
    nextActivity.putExtra(PackageInstallerActivity.EXTRA_ORIGINAL_SOURCE_INFO, sourceInfo);
    nextActivity.putExtra(Intent.EXTRA_ORIGINATING_UID, originatingUid);

    // intent的action为ACTION_VIEW
    if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
        nextActivity.setClass(this, PackageInstallerActivity.class);
    } else {
        Uri packageUri = intent.getData();

        // 通过FileProvider来处理URI的时候会将路径转换为`content://Uri`
        if (packageUri != null && (packageUri.getScheme().equals(ContentResolver.SCHEME_FILE)
                || packageUri.getScheme().equals(ContentResolver.SCHEME_CONTENT))) {
            // 所以下一个要启动的Activity为InstallStaging
            nextActivity.setClass(this, InstallStaging.class);
        } else if (packageUri != null && packageUri.getScheme().equals(
                PackageInstallerActivity.SCHEME_PACKAGE)) {
            nextActivity.setClass(this, PackageInstallerActivity.class);
        } else {
            Intent result = new Intent();
            result.putExtra(Intent.EXTRA_INSTALL_RESULT,
                    PackageManager.INSTALL_FAILED_INVALID_URI);
            setResult(RESULT_FIRST_USER, result);

            nextActivity = null;
        }
    }

    if (nextActivity != null) {
        startActivity(nextActivity);
    }
    finish();
}

  代码中的解释写得挺详细的了,这里就不多做解释。onCreate最后显示的设置跳转到InstallStaging Activity中。下面我们接着分析InstallStaging。

InstallStaging.java

  InstallStaging启动后的调用路径是onCreate->onResume。

protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 设置界面,界面如下图
    setContentView(R.layout.install_staging);

    // 异常退出后需要恢复
    if (savedInstanceState != null) {
        mStagedFile = new File(savedInstanceState.getString(STAGED_FILE));

        if (!mStagedFile.exists()) {
            mStagedFile = null;
        }
    }

    findViewById(R.id.cancel_button).setOnClickListener(view -> {
        if (mStagingTask != null) {
            mStagingTask.cancel(true);
        }
        setResult(RESULT_CANCELED);
        finish();
    });
}

protected void onResume() {
    super.onResume();

    if (mStagingTask == null) {
        if (mStagedFile == null) {
            try {
                // 创建mStagedFile用于存储临时数据
                mStagedFile = TemporaryFileManager.getStagedFile(this);
            } catch (IOException e) {
                showError();
                return;
            }
        }

        // 创建StagingAsyncTask进行异步调用
        mStagingTask = new StagingAsyncTask();
        mStagingTask.execute(getIntent().getData());
    }
}

  install_staging的界面如下:

《(原创)基于Android 8.1之PackageInstaller源码分析(一)》

  其次就是上面代码中并没有太多的实际内容,主要内容还是通过StagingAsyncTask异步调用进行的,下面接着看StagingAsyncTask完成什么操作。

private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> {
    @Override
    protected Boolean doInBackground(Uri... params) {
        if (params == null || params.length <= 0) {
            return false;
        }
        Uri packageUri = params[0];
        // 打开读取Uri指定的文件
        try (InputStream in = getContentResolver().openInputStream(packageUri)) {
            if (in == null) {
                return false;
            }
 
            // 将指定Uri的文件写入到mStagedFile中
            try (OutputStream out = new FileOutputStream(mStagedFile)) {
                byte[] buffer = new byte[1024 * 1024];
                int bytesRead;
                while ((bytesRead = in.read(buffer)) >= 0) {
                    // 响应cancel事件
                    if (isCancelled()) {
                        return false;
                    }
                    out.write(buffer, 0, bytesRead);
                }
            }
        } catch (IOException | SecurityException e) {
            Log.w(LOG_TAG, "Error staging apk from content URI", e);
            return false;
        }
        return true;
    }

    @Override
    protected void onPostExecute(Boolean success) {
        if (success) {
            // 这里通过file协议Uri来调用PackageInstallerActivity来进行安装
            Intent installIntent = new Intent(getIntent());
            installIntent.setClass(InstallStaging.this, PackageInstallerActivity.class);
            installIntent.setData(Uri.fromFile(mStagedFile));
            installIntent
                    .setFlags(installIntent.getFlags() & ~Intent.FLAG_ACTIVITY_FORWARD_RESULT);
            installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
            startActivityForResult(installIntent, 0);
        } else {
            showError();
        }
    }
}

  上面的代码在doInBackground中,将content协议的Uri的内容读取出来然后写入到mStagedFile中,然后接着在onPostExcute中从mStagedFile产生file协议的Uri,然后使用给Uri来调用PackageInstallerActivity。这里感觉就像是启用了StrideMode后,绕了一个大圈,最后还是通过file协议的Uri来调用PackageInstaller。

PackageInstallerActivity.java

protected void onCreate(Bundle icicle) {
    super.onCreate(icicle);

    if (icicle != null) {
        mAllowUnknownSources = icicle.getBoolean(ALLOW_UNKNOWN_SOURCES_KEY);
    }

    // 获取PackageManager
    mPm = getPackageManager();
    // 这里获取的IPackageManager,主要用来和PKMS通信
    mIpm = AppGlobals.getPackageManager();
    // 获取OPSManager
    mAppOpsManager = (AppOpsManager) getSystemService(Context.APP_OPS_SERVICE);
    // 获取PackageInstaller
    mInstaller = mPm.getPackageInstaller();
    // 获取UserManager
    mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);

    final Intent intent = getIntent();

    // 获取Intent中携带的数据
    mCallingPackage = intent.getStringExtra(EXTRA_CALLING_PACKAGE);
    mSourceInfo = intent.getParcelableExtra(EXTRA_ORIGINAL_SOURCE_INFO);
    mOriginatingUid = intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
            PackageInstaller.SessionParams.UID_UNKNOWN);
    mOriginatingPackage = (mOriginatingUid != PackageInstaller.SessionParams.UID_UNKNOWN)
            ? getPackageNameForUid(mOriginatingUid) : null;


    final Uri packageUri;

    // 这里的Action不是CONFIRM_PERMISSION
    if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {
        .....................
    } else {
        mSessionId = -1;
        // 获取file协议的Uri
        packageUri = intent.getData();
        mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
        mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
    }

    if (packageUri == null) {
        Log.w(TAG, "Unspecified source");
        setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);
        finish();
        return;
    }

    // 非wear设备
    if (DeviceUtils.isWear(this)) {
        showDialogInner(DLG_NOT_SUPPORTED_ON_WEAR);
        return;
    }

    // 对Uri进行处理
    boolean wasSetUp = processPackageUri(packageUri);
    if (!wasSetUp) {
        return;
    }

    // 绑定UI界面
    bindUi(R.layout.install_confirm, false);
    checkIfAllowedAndInitiateInstall();
}

  这里首先会先获取到各种各样的管理类,列举如下:

《(原创)基于Android 8.1之PackageInstaller源码分析(一)》

  接着通过processPackageUri对packageUri进行解析

private boolean processPackageUri(final Uri packageUri) {
    mPackageURI = packageUri;

    // 获取Uri使用的协议
    final String scheme = packageUri.getScheme();

    switch (scheme) {
        case SCHEME_PACKAGE: {
            ...................
        } break;

        // ContentResolver.SCHEME_FILE协议
        case ContentResolver.SCHEME_FILE: {
            // 从Uri中获取文件的路径并创建该文件
            File sourceFile = new File(packageUri.getPath());
            // getPackgeInfo最后会通过解析AndroidManifest文件获取apk相关信息
            PackageParser.Package parsed = PackageUtil.getPackageInfo(this, sourceFile);

            // Check for parse errors
            if (parsed == null) {
                Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");
                showDialogInner(DLG_PACKAGE_ERROR);
                setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);
                return false;
            }
            // 对parsed解析生成PackageInfo,mPkgInfo中主要获取Permission相关信息
            mPkgInfo = PackageParser.generatePackageInfo(parsed, null,
                    PackageManager.GET_PERMISSIONS, 0, 0, null,
                    new PackageUserState());
            // 获取应用的label名,如果应用没有定义label则使用应用包名
            mAppSnippet = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);
        } break;

        default: {
            throw new IllegalArgumentException("Unexpected URI scheme " + packageUri);
        }
    }

上面的代码通过解析file协议的Uri获取到文件相关信息保存到pased和mPkgInfo中。现在返回到PackageInstallerActivity的onCreate方法中,现在onCreate还剩下最后一个函数checkIfAllowedAndInitiateInstall还没有看,我们接着分析这个函数

private void checkIfAllowedAndInitiateInstall() {
    // 支持多用户的设备上,首先检查当前用户安装应用的权限
    final int installAppsRestrictionSource = mUserManager.getUserRestrictionSource(
            UserManager.DISALLOW_INSTALL_APPS, Process.myUserHandle());
    if ((installAppsRestrictionSource & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
        showDialogInner(DLG_INSTALL_APPS_RESTRICTED_FOR_USER);
        return;
    } else if (installAppsRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
        startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
        finish();
        return;
    }

    // 允许从未知源安装应用并且安装包来之未知源,则执行安装
    if (mAllowUnknownSources || !isInstallRequestFromUnknownSource(getIntent())) {
        initiateInstall();
    } else {
        // 查看是否未知源应用的安装进行了限制
        final int unknownSourcesRestrictionSource = mUserManager.getUserRestrictionSource(
                UserManager.DISALLOW_INSTALL_UNKNOWN_SOURCES, Process.myUserHandle());
        // 如果从未知源安装应用功能被系统或者用户限制
        if ((unknownSourcesRestrictionSource & UserManager.RESTRICTION_SOURCE_SYSTEM) != 0) {
            showDialogInner(DLG_UNKNOWN_SOURCES_RESTRICTED_FOR_USER);
        // 如果安装未知源应用存在其他的限制,这显示提示信息
        } else if (unknownSourcesRestrictionSource != UserManager.RESTRICTION_NOT_SET) {
            startActivity(new Intent(Settings.ACTION_SHOW_ADMIN_SUPPORT_DETAILS));
            finish();
        } else {
            // 处理未知源应用安装
            handleUnknownSources();
        }
    }
}

这里我们的应用程序属于未知源安装包,所以如果我们在setting中选择了允许未知源安装,那么这里会直接走initiateInstall。如果为从未知源安装应用功能被限制的话,就会显示相关的提示信息。所以接下来我们查看initiateInstall的代码。

private void initiateInstall() {
    // 获取应用包名
    String pkgName = mPkgInfo.packageName;
    // 查看应用是否已经安装过该应用,但是已经被重命名了
    String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });
    if (oldName != null && oldName.length > 0 && oldName[0] != null) {
        pkgName = oldName[0];
        mPkgInfo.packageName = pkgName;
        mPkgInfo.applicationInfo.packageName = pkgName;
    }
    // 检查该应用是否已经安装,如果已经安装显示重安装提示信息
    try {
        // 如果APP只是带有数据,但是被标记为已卸载,这里我们也将其判断为已安装
        mAppInfo = mPm.getApplicationInfo(pkgName,
                PackageManager.MATCH_UNINSTALLED_PACKAGES);
        if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {
            mAppInfo = null;
        }
    } catch (NameNotFoundException e) {
        mAppInfo = null;
    }

    // 显示确认安装界面
    startInstallConfirm();
}

好了,到这里终于到了显示确认安装信息的界面了。

小结

这一篇先到这里了,下一篇我们将从startInstallConfirm。这里我们先做一下这篇文章的小结吧:

  1. 根据Uri的scheme不同,跳转到不同的界面,Android 7.0及以上的版本通过FilePorvider来提供Uri的话,起scheme是content,这里会跳转到InstallStart activity。其余的会跳转到PackageInstallerActivity
  2. InstallStart完成的工作主要就是讲content协议的Uri转换为file协议的Uri,然后见该信息传递给PackageInstallerActivity,然后在跳转到该Activity。
  3. PackageInstallerActivity会对package洗衣和file洗衣的Uri进行处理,如果是file协议的话会解析得到应用的PackageInfo信息
  4. 接着PackageInstallerActivity会对应用的来源进行判断,如果Package的来源于未知源并且系统设置了允许安装未知源的APK就会执行安装,否则显示弹出相关安装未知源应用的提示框

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: