Skip to content

多用户安装

追踪 Android 17 完整安装与 installExistingPackageAsUser 在多用户下的状态、数据目录和广播差异。

基于android-17.0.0_r1
AndroidPMSInstallationMultiUser

多用户安装 ​

本文承接 UMS 与 PMS 和 per-user 包状态,聚焦安装动作如何落到不同用户。这里不重复 APK 解析、签名和 dexopt 主流程,而是回答三个具体问题:完整安装如何选择 newUsers,已存在 APK 如何只为一个用户启用,以及每个用户何时获得数据目录和 PACKAGE_* 广播。

“安装一个 APK”在多用户系统中有两个层次:代码安装是全局的,用户启用是 per-user 的。完整安装可能替换全局 code path,再为一组用户准备数据;installExistingPackageAsUser 不复制 APK,只修改目标用户的 PackageUserState。

1. 两条安装路径 ​

路径APK 是否变化per-user 状态典型入口
完整安装/更新新增或替换 code path根据 request 选择多个 userPackageInstallerSession → InstallPackageHelper
启用已有包不变化仅目标 user 设 installedinstallExistingPackageAsUser

2. 用户选择 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:getNewUsers

java
private int[] getNewUsers(
        InstallRequest installRequest, int[] allUsers)
        throws PackageManagerException {
    final int userId = installRequest.getUserId();
    if (userId != UserHandle.USER_ALL
            && userId != UserHandle.USER_CURRENT
            && !ArrayUtils.contains(allUsers, userId)) {
        throw new PackageManagerException(
                PackageManagerException.INTERNAL_ERROR_MISSING_USER,
                "User " + userId
                        + " doesn't exist or has been removed");
    }

    IntArray newUserIds = new IntArray();
    if (userId != UserHandle.USER_ALL) {
        newUserIds.add(userId);
    } else {
        int[] installedForUsers =
                installRequest.getOriginUsers();
        for (int currentUserId : allUsers) {
            boolean installedForCurrentUser =
                    ArrayUtils.contains(installedForUsers,
                            currentUserId);
            boolean restrictedByPolicy =
                    mPm.isUserRestricted(currentUserId,
                            UserManager.DISALLOW_INSTALL_APPS)
                    || mPm.isUserRestricted(currentUserId,
                            UserManager.DISALLOW_DEBUGGING_FEATURES);
            if (installedForCurrentUser
                    || !restrictedByPolicy) {
                newUserIds.add(currentUserId);
            }
        }
    }
    return newUserIds.toArray();
}

USER_ALL 并不意味着无条件给所有用户安装。已有用户如果已经安装该包会保留在集合中;尚未安装的用户只有在没有相关限制时才加入。反过来,指定 userId 的请求只影响那个用户,并且不存在或已删除的 user 会在提交阶段失败。

3. 提交后的数据准备 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:prepPerformDexoptIfNeeded

java
final int[] allUsers = mPm.mUserManager.getUserIds();
final int[] newUsers = getNewUsers(
        request, allUsers);

mAppDataHelper.prepareAppDataPostCommitLIF(
        ps, 0 /* previousAppId */, newUsers);

if (request.isClearCodeCache()) {
    mAppDataHelper.clearAppDataLIF(
            ps.getPkg(), UserHandle.USER_ALL,
            FLAG_STORAGE_DE | FLAG_STORAGE_CE
                    | FLAG_STORAGE_EXTERNAL
                    | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
}

完整安装在全局包提交之后,才按照 newUsers 准备每个用户的数据。previousAppId=0 明确关闭这条路径的数据迁移;用户级 DE/CE 是否实际创建,还由 AppDataHelper 根据用户运行/解锁状态决定。

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:commitPackagesLocked

java
AndroidPackage pkg = commitReconciledScanResultLocked(
        reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);

PackageSetting ps = mPm.mSettings
        .getPackageLPr(packageName);
if (ps != null) {
    installRequest.setNewUsers(
            ps.queryInstalledUsers(allUsers, true));
}

if (installRequest.getReturnCode()
        == PackageManager.INSTALL_SUCCEEDED) {
    mPm.updateSequenceNumberLP(
            ps, installRequest.getNewUsers());
}

提交后重新查询 queryInstalledUsers 得到最终 new users,而不是完全相信请求初始参数。归档安装还可能在最后把包标为 archived/not installed,因此“安装成功”与“哪些用户最终 installed=true”要以提交后的 PackageSetting 为准。

4. 启用已有 APK ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:installExistingPackageAsUser

java
public Pair<Integer, IntentSender>
        installExistingPackageAsUser(
                @Nullable String packageName,
                @UserIdInt int userId,
                @InstallFlags int installFlags,
                @InstallReason int installReason,
                @Nullable List<String> allowlistedPermissions,
                @Nullable IntentSender intentSender) {
    final int callingUid = Binder.getCallingUid();
    if (mContext.checkCallingOrSelfPermission(
            android.Manifest.permission.INSTALL_PACKAGES)
            != PackageManager.PERMISSION_GRANTED
            && mContext.checkCallingOrSelfPermission(
            android.Manifest.permission.INSTALL_EXISTING_PACKAGES)
            != PackageManager.PERMISSION_GRANTED) {
        throw new SecurityException(
                "Neither caller has install permission");
    }

    Computer preLockSnapshot = mPm.snapshotComputer();
    preLockSnapshot.enforceCrossUserPermission(
            callingUid, userId, true, true,
            "installExistingPackage for user " + userId);
    if (mPm.isUserRestricted(userId,
            UserManager.DISALLOW_INSTALL_APPS)) {
        return Pair.create(
                PackageManager.INSTALL_FAILED_USER_RESTRICTED,
                intentSender);
    }

这条入口需要安装权限、跨用户权限和目标用户未被 DISALLOW_INSTALL_APPS 限制。它的安全边界比普通应用启动更严格,因为它会改变目标用户的 installed 状态。

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:状态修改分支

java
synchronized (mPm.mLock) {
    PackageSetting ps = mPm.mSettings
            .getPackageLPr(packageName);
    if (ps == null || ps.getPkg() == null) {
        return Pair.create(
                PackageManager.INSTALL_FAILED_INVALID_URI,
                intentSender);
    }

    if (!ps.getInstalled(userId)) {
        ps.setInstalled(true, userId);
        ps.setHidden(false, userId);
        ps.setInstallReason(installReason, userId);
        ps.setUninstallReason(
                PackageManager.UNINSTALL_REASON_UNKNOWN,
                userId);
        ps.setFirstInstallTime(
                System.currentTimeMillis(), userId);
        mPm.mSettings.writePackageRestrictionsLPr(userId);
        mPm.mSettings.writeKernelMappingLPr(ps);
        installed = true;
    }
}

启用已有包的核心只有 per-user 状态变更:不会重新解析 APK、重新分配 appId 或重建全局 package record。若目标用户已经 installed,普通调用不会重复安装;INSTALL_FULL_APP 只在 Instant App 转 full app 等特殊分支继续处理。

5. Instant 与归档 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:Instant/full 分支

java
final boolean instantApp =
        (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0;
final boolean fullApp =
        (installFlags & PackageManager.INSTALL_FULL_APP) != 0;

if (instantApp && (ps.isSystem()
        || ps.isUpdatedSystemApp()
        || isPackageDeviceAdmin
        || isProtectedPackage)) {
    return Pair.create(
            PackageManager.INSTALL_FAILED_INVALID_URI,
            intentSender);
}

ScanPackageUtils.setInstantAppForUser(
        mPm.mInjector, ps, userId,
        instantApp, fullApp);

Instant/full 不是全局 APK 的两个版本,而是 per-user 的安装属性。系统包、设备管理员或受保护包不能通过这个入口转成 Instant App;状态修改完成后,后续可见性、权限和广播路径都会按目标用户的 instant 标志判断。

6. 数据目录时机 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:启用已有包的数据准备

java
if (installed && pkgSetting.getPkg() != null) {
    mPm.mPermissionManager.onPackageInstalled(
            pkgSetting.getPkg(), Process.INVALID_UID,
            permissionParamsBuilder.build(), userId);

    try (PackageManagerTracedLock lock =
            mPm.mInstallLock.acquireLock()) {
        mAppDataHelper.prepareAppDataPostCommitLIF(
                pkgSetting, 0, new int[]{userId});
    }
}

数据准备发生在 per-user state 提交之后。AppDataHelper 根据用户状态和 storage flags 决定 DE/CE 创建时机:用户未解锁时不能假定 CE key 可用,用户尚未运行时也可能只记录待准备状态。调用者不能把 setInstalled(true) 当作“应用目录已经全部存在”。

7. 单用户广播 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:启用已有包的广播

java
boolean isArchived = pkgSetting.getPkg() == null;
mBroadcastHelper.sendPackageAddedForUser(
        mPm.snapshotComputer(), packageName,
        pkgSetting, userId, isArchived,
        DataLoaderType.NONE,
        mPm.mAppPredictionServicePackage);

synchronized (mPm.mLock) {
    mPm.updateSequenceNumberLP(
            pkgSetting, new int[]{userId});
}

installExistingPackageAsUser 只给目标 user 调用 sendPackageAddedForUser,随后更新该 user 的 sequence number。广播发送发生在数据准备后,接收者看到的包状态应已经是 installed;但这不意味着所有用户都收到广播。

源码文件:frameworks/base/services/core/java/com/android/server/pm/BroadcastHelper.java,符号:sendPackageAddedForUser

java
void sendPackageAddedForUser(
        @NonNull Computer snapshot,
        String packageName, PackageSetting ps,
        int userId, boolean isArchived,
        int dataLoaderType,
        String appPredictionServicePackage) {
    sendPackageBroadcastAndNotify(
            Intent.ACTION_PACKAGE_ADDED, packageName,
            ps, userId, isArchived, dataLoaderType,
            appPredictionServicePackage);
}

真正的接收者过滤仍由 sendPackageBroadcastAndNotify、AppsFilter 和用户广播机制完成。包已 installed 不等于任意接收者都能看到它;包可见性和 receiver 权限仍然生效。

8. 更新用户 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:commitPackagesLocked

java
if (installRequest.isInstallReplace()) {
    PackageStateInternal oldState =
            mPm.snapshotComputer()
                    .getPackageStateInternal(packageName);
    installRequest
            .setScannedPackageSettingFirstInstallTimeFromReplaced(
                    oldState, allUsers);
    installRequest.getRemovedInfo().mBroadcastAllowList =
            mPm.mAppsFilter.getVisibilityAllowList(
                    mPm.snapshotComputer(),
                    installRequest.getScannedPackageSetting(),
                    allUsers,
                    mPm.mSettings.getPackagesLocked());
}

AndroidPackage pkg = commitReconciledScanResultLocked(
        reconciledPkg, allUsers);
updateSettingsLI(pkg, allUsers, installRequest);

更新安装会记录原包的 per-user 首次安装时间,并构造广播 allowlist。它不能简单地把所有 user 都当成首次安装:已 installed 用户走替换语义,未 installed 用户才可能在提交后进入 new users。

9. 广播差异 ​

广播 action 由安装结果和用户原状态共同决定:

情况代码路径用户级结果
新 APK 首次安装到 usernew user in InstallRequestPACKAGE_ADDED
已安装 user 的 APK 更新replace + broadcast allowlistPACKAGE_REPLACED 等更新广播
已有 APK 启用到 userinstallExistingPackageAsUser目标 user PACKAGE_ADDED
APK 全局替换但 user 未安装不加入该 user 的 installed 集合不应伪造该 user 的普通 update 广播

源码文件:frameworks/base/services/core/java/com/android/server/pm/BroadcastHelper.java,符号:广播分派相关方法

java
sendPackageBroadcastAndNotify(
        Intent.ACTION_PACKAGE_ADDED,
        packageName, extras, targetUserId,
        finishedReceiver);

sendPackageBroadcastAndNotify(
        Intent.ACTION_PACKAGE_REPLACED,
        packageName, extras, targetUserId,
        finishedReceiver);

广播不是全局“一次发送”:目标 user、包可见性 allowlist、receiver 权限和 action 都参与最终分发。排查“某用户没有收到更新广播”时,先检查该 user 是否 installed、allowlist 是否包含 receiver,再看 receiver 自己的 manifest 过滤器。

10. 权限与恢复 ​

installExistingPackageAsUser 在修改 installed 状态后调用 PermissionManagerServiceInternal.onPackageInstalled,并通过 restoreAndPostInstall 处理备份恢复、角色更新和可选 IntentSender 回调。它不会因为 APK 已经存在就跳过用户级权限初始化。

源码文件:frameworks/base/services/core/java/com/android/server/pm/InstallPackageHelper.java,符号:restoreAndPostInstall

java
InstallRequest request = new InstallRequest(
        userId, PackageManager.INSTALL_SUCCEEDED,
        pkgSetting.getPkg(), new int[]{userId},
        () -> mPm.restorePermissionsAndUpdateRolesForNewUserInstall(
                packageName, userId),
        pkgSetting.getAppId(), callingUid,
        pkgSetting.isSystem());
restoreAndPostInstall(request);

用户级启用的“安装完成”不止是广播:权限恢复和角色更新在 post-install 回调中执行。调用方拿到成功码时,部分异步恢复工作可能仍在进行。

11. 失败路径 ​

失败点返回/行为
无 INSTALL_PACKAGES / INSTALL_EXISTING_PACKAGESSecurityException
跨用户权限不足SecurityException
DISALLOW_INSTALL_APPSINSTALL_FAILED_USER_RESTRICTED
包不存在或无解析包INSTALL_FAILED_INVALID_URI
Instant App 受系统/设备管理员保护INSTALL_FAILED_INVALID_URI
指定用户不存在完整安装阶段 INTERNAL_ERROR_MISSING_USER
数据目录准备失败安装请求错误,后续安装流程停止

安装的全局阶段成功,不代表每个 user 的数据准备、权限恢复和广播都成功。反之,installExistingPackageAsUser 不会因为没有 APK 拷贝而绕过跨用户权限和 user restriction。

12. 阅读检查 ​

读者可以复述两条主线:

  1. 完整安装:InstallRequest.userId → getNewUsers → 全局 commit → queryInstalledUsers → prepareAppDataPostCommitLIF → per-user 广播。
  2. 已有包启用:权限/跨用户检查 → PackageSetting.setInstalled(true, userId) → restrictions/kernel mapping → PermissionManager → app data → sendPackageAddedForUser → restore/roles。

然后判断:安装 APK 到 user 0 会自动把 installed=true 写给所有用户吗?更新已安装用户和启用已有包使用完全相同的广播路径吗?数据目录在 setInstalled(true) 的同一行就一定全部创建了吗?正确答案分别是“不一定”“不完全相同”“不一定”。