Skip to content

包挂起与恢复

追踪 Android 17 包挂起的策略校验、多个挂起方、状态提交、广播与恢复路径。

基于android-17.0.0_r1
AndroidPMSSuspendPackageHelperPackageUserStateDevicePolicy

包挂起与恢复 ​

本文面向已经读过 PackageStateMutator 事务提交、ProtectedPackages 数据保护 和 PackageSetting 数据结构 的读者。本文不讲 pm 命令,而是从 setPackagesSuspendedAsUser() 进入 SuspendPackageHelper,解释一个包为什么可能被多个管理者同时挂起、解除一个管理者后为什么仍保持 suspended,以及状态如何通过 restrictions 文件和广播到达消费者。

1. 挂起模型 ​

源码文件:

  • frameworks/base/services/core/java/com/android/server/pm/SuspendPackageHelper.java
  • frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java

Android 17 不用一个 suspended 布尔值记录“谁挂起了包”,而是在 per-user state 保存 UserPackage -> SuspendParams 映射。只要映射非空,isSuspended() 就为 true;每个挂起方可以携带 dialog、app extras、launcher extras 和 quarantine 标志。

2. Binder 入口 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:IPackageManagerImpl.setPackagesSuspendedAsUser。

java
public String[] setPackagesSuspendedAsUser(String[] packageNames,
        boolean suspended, PersistableBundle appExtras,
        PersistableBundle launcherExtras, SuspendDialogInfo dialogInfo,
        int flags, String suspendingPackage,
        int suspendingUserId, int targetUserId) {
    final int callingUid = Binder.getCallingUid();
    final boolean quarantined = (flags & FLAG_SUSPEND_QUARANTINED) != 0
            && Flags.quarantinedEnabled();
    final Computer snapshot = snapshotComputer();
    final UserPackage suspender = UserPackage.of(
            suspendingUserId, suspendingPackage);
    enforceCanSetPackagesSuspendedAsUser(snapshot, quarantined,
            suspender, callingUid, targetUserId,
            "setPackagesSuspendedAsUser");
    return mSuspendPackageHelper.setPackagesSuspended(
            snapshot, packageNames, suspended, appExtras,
            launcherExtras, dialogInfo, suspender,
            targetUserId, callingUid, quarantined);
}

入口先生成 snapshot,再确定挂起方的 UserPackage 身份。跨 user 是否允许由 feature flag 和调用者策略共同决定;不能把 suspendingPackage 字符串当成可信身份,它和 userId 一起组成映射 key。

3. 用户限制 ​

SuspendPackageHelper 在 suspended=true 且不是 quarantine 时调用 isSuspendAllowedForUser。普通调用者必须满足目标用户没有 DISALLOW_APPS_CONTROL 和 DISALLOW_UNINSTALL_APPS;Device Policy Controller 可以绕过这两个用户限制。限制命中时方法直接把输入包名作为失败数组返回,不写任何 state。

这与单包不可挂起不同:前者是整个 user 的策略拒绝,后者是逐包的保护规则。调用者必须逐项处理返回的 failed package names。

4. 不可挂起包 ​

源码符号:canSuspendPackageForUser。

PMS 会拒绝挂起以下关键包:

  • active device admin;
  • 当前默认 launcher;
  • required installer/uninstaller/verifier;
  • 默认 dialer;
  • required PermissionController/developer verification service;
  • ProtectedPackages 判定的 protected package;
  • 当前挂起方自身。
java
if (mPm.isPackageDeviceAdmin(packageName, targetUserId)) {
    continue;
}
if (packageName.equals(activeLauncherPackageName)) {
    continue;
}
if (packageName.equals(requiredInstallerPackage)
        || packageName.equals(requiredUninstallerPackage)
        || packageName.equals(requiredVerifierPackage)) {
    continue;
}
if (mProtectedPackages.isPackageStateProtected(
        targetUserId, packageName)) {
    continue;
}

这些检查依赖 target user 的 default app、KnownPackages 和 package state snapshot。一个包在 user 0 可挂起,不代表它在工作资料 user 10 也可挂起。

5. 多挂起方 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/SuspendPackageHelper.java;符号:setPackagesSuspended。

对每个 package,helper 用当前 suspendingPackage 在 suspendParamsMap 中查找旧参数。挂起时写入新参数,解除时只移除该挂起方:

java
if (suspended) {
    userState.putSuspendParams(suspendingPackage, newSuspendParams);
} else {
    userState.removeSuspension(suspendingPackage);
}

如果 A 和 B 都挂起同一个包,解除 A 后 map 仍有 B,包继续 suspended。只有最后一个挂起方被移除,isSuspended() 才变为 false,并发送 fully-unsuspended 的通知。

6. 提交与返回值 ​

helper 先收集 changedPackagesList、需要广播的包和失败包,再用一次 commitPackageStateMutation(null, ...) 批量写入 user state。返回数组只包含不可修改项;已挂起且参数未变化的包仍会进入 notify list,以兼容既有通知行为。

java
if (suspended && !changed) {
    // 即使状态没变,兼容行为仍要求发送通知
    notifyPackagesList.add(packageName);
    notifyUids.add(UserHandle.getUid(targetUserId,
            packageState.getAppId()));
    continue;
}

setPackagesSuspendedAsUser 返回成功数组不表示广播已经发送;helper 的 mutation、restrictions 写盘和 broadcast 是连续但分离的阶段。

7. 广播与消费者 ​

状态变化后有三类通知:

  • ACTION_PACKAGES_SUSPENDED / ACTION_PACKAGES_UNSUSPENDED:面向 user 的包变化;
  • ACTION_MY_PACKAGE_SUSPENDED:通知被挂起应用自身;
  • ACTION_PACKAGES_SUSPENSION_CHANGED:即使仍有其他挂起方,也通知参数发生了变化。

Launcher extras 和 app extras 都来自各挂起方参数;读取时 helper 会合并当前 user state 中所有 SuspendParams 的对应 Bundle。因此多挂起方同时存在时,消费者看到的是合并结果,而不是某个单一管理者的值。

8. 持久化与恢复 ​

挂起参数由 PackageUserStateImpl 写入每个 user 的 package-restrictions.xml,包括 suspending package 的 userId、package name、dialog 和 extras。SuspendPackageHelper 在成功通知后调用 scheduleWritePackageRestrictions(targetUserId);如果调用方要求立即一致性,则通过 flushPackageRestrictions 同步刷盘。

包删除、suspender 卸载或 DPC 状态变化时,PMS 可以调用 removeSuspensionsBySuspendingPackage。该方法按 predicate 删除匹配挂起方,只有全部参数移除的包才发送 ACTION_PACKAGES_UNSUSPENDED。

9. 查询与启动 ​

挂起不会等同于 hidden、disabled 或 package visibility。包仍可出现在 PackageManager 查询中,但启动、launcher UI、系统服务和应用自身会根据 PackageUserState.isSuspended() 读取对话框和 extras 并限制行为。

getSuspendedPackageAppExtras 和 getSuspendedPackageLauncherExtras 会遍历全部 SuspendParams 并合并 Bundle。若包只是被 A 挂起、B 仍存在,解除 A 后这些 extras 仍来自 B;只有 map 为空才返回 null。

10. 失败定位 ​

现象检查点解释
返回全部失败包user restriction / DPC / target user整个用户策略阻止挂起
单个关键包无法挂起canSuspendPackageForUserlauncher、installer、dialer、admin、protected 都有保护
unsuspend 后仍 suspendedsuspendParams 是否还有其他 suspender解除只移除当前挂起方
状态变了但 launcher 未更新三类广播与 handler/receivermutation 和广播不是同一时点
extras 内容异常多个 SuspendParams 的合并顺序app/launcher extras 按挂起方合并
重启后恢复挂起user restrictions XMLstate 属于 per-user 持久化,不是进程内临时标记

11. 源码练习 ​

  1. 构造 A/B 两个 suspender 先后挂起同一 user 的包,追踪解除 A 后 isSuspended() 和广播类型。
  2. 对比用户限制拒绝和 launcher 保护拒绝,说明为什么一个返回整组失败数组,另一个只标记对应 package。
  3. 从 setPackagesSuspended 追踪 app extras 到 getSuspendedPackageAppExtras,解释多个挂起方的 Bundle 合并边界。
  4. 给定 DPC 卸载后包仍 suspended,定位 removeSuspensionsBySuspendingPackage 的 predicate、mutation 和 unsuspend 广播。