包挂起与恢复
本文面向已经读过 PackageStateMutator 事务提交、ProtectedPackages 数据保护 和 PackageSetting 数据结构 的读者。本文不讲 pm 命令,而是从 setPackagesSuspendedAsUser() 进入 SuspendPackageHelper,解释一个包为什么可能被多个管理者同时挂起、解除一个管理者后为什么仍保持 suspended,以及状态如何通过 restrictions 文件和广播到达消费者。
1. 挂起模型
源码文件:
frameworks/base/services/core/java/com/android/server/pm/SuspendPackageHelper.javaframeworks/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。
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;- 当前挂起方自身。
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 中查找旧参数。挂起时写入新参数,解除时只移除该挂起方:
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,以兼容既有通知行为。
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 | 整个用户策略阻止挂起 |
| 单个关键包无法挂起 | canSuspendPackageForUser | launcher、installer、dialer、admin、protected 都有保护 |
| unsuspend 后仍 suspended | suspendParams 是否还有其他 suspender | 解除只移除当前挂起方 |
| 状态变了但 launcher 未更新 | 三类广播与 handler/receiver | mutation 和广播不是同一时点 |
| extras 内容异常 | 多个 SuspendParams 的合并顺序 | app/launcher extras 按挂起方合并 |
| 重启后恢复挂起 | user restrictions XML | state 属于 per-user 持久化,不是进程内临时标记 |
11. 源码练习
- 构造 A/B 两个 suspender 先后挂起同一 user 的包,追踪解除 A 后
isSuspended()和广播类型。 - 对比用户限制拒绝和 launcher 保护拒绝,说明为什么一个返回整组失败数组,另一个只标记对应 package。
- 从
setPackagesSuspended追踪 app extras 到getSuspendedPackageAppExtras,解释多个挂起方的 Bundle 合并边界。 - 给定 DPC 卸载后包仍 suspended,定位
removeSuspensionsBySuspendingPackage的 predicate、mutation 和 unsuspend 广播。
