分心限制机制
本文承接 包挂起与恢复 和 PackageStateMutator 事务提交,讨论另一套容易与 suspension 混淆的 per-user 状态:distraction restrictions。它不把应用整体挂起,而是用 bitmask 分别影响启动建议、通知展示以及带确认页的启动。读完后,读者应能从 Binder 入口追到 PackageUserStateImpl.mDistractionFlags,再分别定位 NotificationManagerService 和 ActivityTaskManager 的消费者。
1. 三个位
源码文件:frameworks/base/core/java/android/content/pm/PackageManager.java。
Android 17 定义三个可组合标志:
public static final int RESTRICTION_NONE = 0x0;
public static final int RESTRICTION_HIDE_FROM_SUGGESTIONS = 0x00000001;
public static final int RESTRICTION_HIDE_NOTIFICATIONS = 0x00000002;
public static final int RESTRICTION_CONFIRM_WITH_SPEEDBUMP = 0x00000004;0x1 是从推荐中隐藏,0x2 才是隐藏通知。三个值由 @IntDef(flag = true) 声明,可以按位组合,例如 0x3 同时影响推荐和通知。RESTRICTION_NONE 用于清除全部限制,不是“保留原标志不变”。
2. 状态 owner
源码文件:
frameworks/base/services/core/java/com/android/server/pm/DistractingPackageHelper.javaframeworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java
DistractingPackageHelper 负责策略检查、批量 mutation 和广播;真正状态是 PackageUserStateImpl 中的单个 mDistractionFlags。它没有 suspension 那样的 suspendingPackage -> SuspendParams 映射,因此后一次写入会整体替换旧 bitmask,也无法记录哪个管理者设置了哪一位。
3. Binder 权限
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:setDistractingPackageRestrictionsAsUser。
public String[] setDistractingPackageRestrictionsAsUser(
String[] packageNames, int restrictionFlags, int userId) {
final int callingUid = Binder.getCallingUid();
final Computer snapshot = snapshotComputer();
enforceCanSetDistractingPackageRestrictionsAsUser(
callingUid, userId,
"setDistractingPackageRestrictionsAsUser");
Objects.requireNonNull(packageNames, "packageNames cannot be null");
return mDistractingPackageHelper
.setDistractingPackageRestrictionsAsUser(
snapshot, packageNames, restrictionFlags,
userId, callingUid);
}调用者必须拥有 SUSPEND_APPS。非 system/root 调用者只能操作自己的 user;这里没有把跨用户权限和普通同用户权限混成一个检查。返回值是未能修改的 package 数组,不是成功列表。
4. 用户级总拒绝
源码文件:frameworks/base/services/core/java/com/android/server/pm/DistractingPackageHelper.java;符号:setDistractingPackageRestrictionsAsUser。
设置非零限制时,helper 复用 SuspendPackageHelper.isSuspendAllowedForUser。如果用户有 DISALLOW_APPS_CONTROL 或 DISALLOW_UNINSTALL_APPS,且调用者不是该 user 的 DPC,整批 package 原样作为失败数组返回。清除限制时 restrictionFlags == RESTRICTION_NONE,不会被这条用户限制阻止。
if (restrictionFlags != RESTRICTION_NONE
&& !mSuspendPackageHelper.isSuspendAllowedForUser(
snapshot, userId, callingUid)) {
return packageNames;
}这个不对称设计保证管理限制可以阻止新增控制,但不会把已经存在的 distraction flags 永久锁死。
5. 关键包过滤
非零限制继续调用 canSuspendPackageForUser,所以 active device admin、默认 launcher、installer、uninstaller、verifier、默认 dialer、PermissionController 和 protected package 等关键包也不能被标记为 distracting。清除限制不调用 canSuspendPackageForUser,允许这些包恢复到 RESTRICTION_NONE。
随后 helper 用 getPackageStateForInstalledAndFiltered 同时检查目标包存在、已为该 user 安装且对调用者可见。失败项加入 unactionedPackages,其余项只有在旧 bitmask 与新 bitmask 不同时才进入 mutation 集合。
6. 批量提交
源码文件:frameworks/base/services/core/java/com/android/server/pm/DistractingPackageHelper.java;符号:changesToCommit mutation。
mPm.commitPackageStateMutation(null, mutator -> {
for (int index = 0; index < changesToCommit.size(); index++) {
mutator.forPackage(changesToCommit.valueAt(index))
.userState(userId)
.setDistractionFlags(restrictionFlags);
}
});这里传入 null initial state,表示不做 optimistic sequence 校验;策略判断基于入口 snapshot,写入时依赖 PMS 写锁。bitmask 是替换而不是增量 OR:调用者若想在现有 0x1 上增加通知限制,必须提交 0x3,只提交 0x2 会清掉 0x1。
7. 广播过滤
状态变化后,BroadcastHelper.sendDistractingPackagesChanged 异步发送 ACTION_DISTRACTING_PACKAGES_CHANGED,extras 包含 package names、UIDs 和完整 restriction bitmask。广播使用 FLAG_RECEIVER_REGISTERED_ONLY,并按接收者 UID 过滤 package 列表,避免通过广播泄露不可见包。
源码文件:frameworks/base/services/core/java/com/android/server/pm/BroadcastHelper.java。
extras.putStringArray(Intent.EXTRA_CHANGED_PACKAGE_LIST, pkgList);
extras.putIntArray(Intent.EXTRA_CHANGED_UID_LIST, uidList);
extras.putInt(Intent.EXTRA_DISTRACTION_RESTRICTIONS,
distractionFlags);
mHandler.post(() -> sendPackageBroadcast(
Intent.ACTION_DISTRACTING_PACKAGES_CHANGED,
null, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY,
..., (callingUid, intentExtras) ->
filterExtrasChangedPackageList(
snapshotSupplier, callingUid, intentExtras), ...));mutation 已完成后才排队广播,因此 PMS 的新 snapshot 可以先看到状态,而外部接收者稍后才刷新缓存。
8. 通知消费者
源码文件:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java。
NotificationManagerService 接收 distraction 广播,只检查 RESTRICTION_HIDE_NOTIFICATIONS 位。该位存在时隐藏相关 UID 的通知;不存在时执行 unhide。它不关心 suggestions 或 speedbump 位。
int restrictions = intent.getIntExtra(
Intent.EXTRA_DISTRACTION_RESTRICTIONS,
PackageManager.RESTRICTION_NONE);
if ((restrictions & RESTRICTION_HIDE_NOTIFICATIONS) != 0) {
hideNotifications = true;
} else {
unhideNotifications = true;
}“隐藏”不等于取消,源码明确设置 cancelNotifications = false。解除 flag 后已有通知可以重新显示,而不是要求应用重新发一遍。
9. 启动确认页
源码文件:frameworks/base/services/core/java/com/android/server/wm/ActivityStartInterceptor.java;符号:interceptDistractingPackageIfNeeded。
speedbump 只有在 activityStartInterceptorSpeedbumps() flag 开启时消费。ActivityStartInterceptor 通过 PackageManagerInternal.getDistractingPackageRestrictions 读取目标 user 的 bitmask,检查 RESTRICTION_CONFIRM_WITH_SPEEDBUMP,然后把启动重定向到 Digital Wellbeing 的 ACTION_WELLBEING_CONFIRM_WITH_SPEEDBUMP。
int restrictions = pmi.getDistractingPackageRestrictions(
packageName, mUserId);
if ((restrictions & RESTRICTION_CONFIRM_WITH_SPEEDBUMP) == 0) {
return false;
}
String wellbeingPkg = mServiceContext.getPackageManager()
.getWellbeingPackageName();
if (wellbeingPkg == null) {
return false;
}没有 Wellbeing package、确认 Intent 无法解析,或真实调用者就是 Wellbeing 自身时,拦截器返回 false,原启动继续。这证明 flag 表示“请求确认”,不是无条件禁止启动。
10. 持久化与移除
PackageUserStateImpl.mDistractionFlags 写入该 user 的 package-restrictions.xml 属性 distraction_flags;零值不写属性。helper 在广播后调用 scheduleWritePackageRestrictions(userId),所以内存状态、广播和磁盘文件存在时间差。
内部清理使用 removeDistractingPackageRestrictions:它只挑选当前 flags 非零的包,批量写为 RESTRICTION_NONE,发送同一个 changed 广播并安排写盘。禁用一个拥有 SUSPEND_APPS 的 package 时,PMS 会调用该清理路径,避免失效的管理者留下永久限制。
11. 挂起边界
| 维度 | Distraction restriction | Suspension |
|---|---|---|
| 状态结构 | 单个 bitmask | 多个 suspender 到 SuspendParams 的 map |
| 设置者归属 | 不记录 | 记录 UserPackage |
| 解除方式 | 写 RESTRICTION_NONE 清全部 | 只移除指定 suspender |
| 启动影响 | 可选 speedbump | 通常阻止/替代正常启动 |
| 通知影响 | 0x2 独立隐藏 | suspended 广播触发隐藏 |
| 持久化 | distraction_flags 属性 | suspended params 子节点 |
两者复用关键包可操作性检查,但状态模型和消费者完全不同。
12. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| 整批返回失败 | user restrictions 与 DPC 身份 | 非零限制被用户策略整体拒绝 |
| 个别包失败 | canSuspendPackageForUser、安装和 visibility | 关键包或不可见包逐项失败 |
设置 0x2 后 suggestions 恢复 | bitmask 是替换写入 | 旧 0x1 被清除,应提交 0x3 |
| 通知仍可见 | 广播延迟、NMS receiver、目标 user | NMS 通过异步广播消费新位 |
| speedbump 不出现 | feature flag、Wellbeing package、Intent resolve | flag 不保证拦截一定发生 |
| shell 显示 UNKNOWN | 多位组合不是单一 switch case | 应按位解析数值而非依赖字符串 |
13. 源码练习
- 从
setDistractingPackageRestrictionsAsUser追到 NMS,标出权限、关键包检查、mutation、广播和通知隐藏的五个时点。 - 以旧值
0x1、新值0x2为输入,说明为什么结果不是0x3,以及调用方怎样保留原限制。 - 给定
0x4已写入但启动没有确认页,按 feature flag、Wellbeing UID、Intent resolve 三个分支定位。 - 对比 distraction 和 suspension 的解除流程,说明为什么前者不能按某个管理者单独撤销。
