分心限制消费者
本文承接 分心限制机制 和 包挂起与恢复。上一篇讲状态如何写入 PMS;本文继续追踪状态离开 PMS 后由谁消费、何时生效,以及为什么同一个 restriction bitmask 会在不同服务中产生不同结果。文章不讨论 Digital Wellbeing 如何决定策略,只讨论 Android 17 中已经确定的执行链。
1. Bitmask 语义
源码文件:frameworks/base/core/java/android/content/pm/PackageManager.java。
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;这是 flag 集合,不是互斥枚举。0x3 同时表示隐藏建议和隐藏通知;0x4 请求启动确认页。PMS 原样存储整数,消费者各自使用按位与读取自己关心的位。
2. 写入与广播
源码文件:frameworks/base/services/core/java/com/android/server/pm/DistractingPackageHelper.java。
helper 先用 snapshot 校验 user restriction、关键包、安装状态和调用者可见性,再把发生变化的包收集到 changesToCommit。一次 mutation 将每个目标 user state 的 distractionFlags 替换为新值。
mPm.commitPackageStateMutation(null, mutator -> {
for (int index = 0; index < changesToCommit.size(); index++) {
mutator.forPackage(changesToCommit.valueAt(index))
.userState(userId)
.setDistractionFlags(restrictionFlags);
}
});只有 oldDistractionFlags != restrictionFlags 的包进入 changed list。写入完成后,BroadcastHelper 异步发送 ACTION_DISTRACTING_PACKAGES_CHANGED,extras 同时携带 package names、UIDs、完整 bitmask 和 user。
广播是通知机制,不是状态提交本身。接收者可能晚于 PMS snapshot 看到新值;调试时应把 mutation 时间、广播投递时间和消费者读取时间分开。
3. 通知隐藏
源码文件:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java。
NMS 监听 ACTION_DISTRACTING_PACKAGES_CHANGED,只检查 RESTRICTION_HIDE_NOTIFICATIONS。该位存在时隐藏目标 UID 的通知;不存在时解除隐藏。源码还把 cancelNotifications 设为 false,因此这是可逆的隐藏状态,不是删除通知记录。
int distractionRestrictions = intent.getIntExtra(
Intent.EXTRA_DISTRACTION_RESTRICTIONS,
PackageManager.RESTRICTION_NONE);
if ((distractionRestrictions
& PackageManager.RESTRICTION_HIDE_NOTIFICATIONS) != 0) {
hideNotifications = true;
} else {
unhideNotifications = true;
}NMS 不处理 HIDE_FROM_SUGGESTIONS 和 CONFIRM_WITH_SPEEDBUMP。因此设置 0x1 后通知不应被隐藏;设置 0x2 后建议列表不一定变化。
4. 启动确认
源码文件:frameworks/base/services/core/java/com/android/server/wm/ActivityStartInterceptor.java;符号:interceptDistractingPackageIfNeeded。
启动拦截首先受 Flags.activityStartInterceptorSpeedbumps() 控制;flag 关闭时直接返回 false。开启后通过 PackageManagerInternal.getDistractingPackageRestrictions 读取目标 user 的 mask,只有包含 CONFIRM_WITH_SPEEDBUMP 才继续。
int restrictions = pmi.getDistractingPackageRestrictions(
packageName, mUserId);
if ((restrictions & RESTRICTION_CONFIRM_WITH_SPEEDBUMP) == 0) {
return false;
}拦截器随后查找 Wellbeing package,构造 ACTION_WELLBEING_CONFIRM_WITH_SPEEDBUMP,并把原始启动封装为 one-shot immutable IntentSender。若 Wellbeing package 不存在、UID 无效、调用者本身就是 Wellbeing,或确认 Intent 无法解析,函数返回 false,原始启动继续。
这条路径是“启动前确认”,不是 PackageManager 把组件标记为 disabled。组件 resolver 仍可以返回目标 Activity,真正的分流发生在 ActivityTaskManager 启动阶段。
5. 建议列表
Android 17 的 PackageManager 常量明确说明 HIDE_FROM_SUGGESTIONS 用于从用户建议中隐藏包;PMS 将 bit 写入 user state,但建议列表消费者不在 PMS 内。启动器、分享面板或系统建议服务需要通过各自的 PackageManager 查询读取该 user state,并自行按位过滤。
这解释了为什么 ACTION_DISTRACTING_PACKAGES_CHANGED 的 extras 是完整 mask:接收者可以只处理自己支持的位。PMS 不为每种 UI 发送不同广播,也不假设所有接收者都理解 speedbump。
6. 清除限制
设置 RESTRICTION_NONE 时,helper 跳过 isSuspendAllowedForUser 和关键包 canSuspendPackageForUser 检查,只验证包存在且对调用者可见,然后将 flags 写零并发送 changed 广播。这样即使 user policy 已阻止“新增限制”,系统仍能移除旧限制。
PMS 的 removeAllDistractingPackageRestrictions 会遍历 user 的所有 available package,调用同一 helper 清零。禁用一个拥有 SUSPEND_APPS 的管理包、策略撤销或用户恢复时,都会使用这条全量清理路径。
7. 持久化与读取
源码文件:frameworks/base/services/core/java/com/android/server/pm/Settings.java、frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java。
非零 flags 写入该 user 的 package-restrictions.xml:
<package name="com.example.app" distraction_flags="3" />零值不写属性。读取时 Settings 将属性解析为 PackageUserStateImpl.setDistractionFlags;因此重启后通知隐藏、建议过滤和 speedbump 都依赖 user restrictions 是否成功写盘。PMS 的内存 mutation、scheduleWritePackageRestrictions 和消费者广播不是同一时刻完成。
8. Shell 观察
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerShellCommand.java。
pm set-distracting 通过多个 --flag 累加 bit;pm get-distracting 返回整数并调用 stateToString。当前 stateToString 对组合值没有逐位展开,0x3 或 0x6 可能显示为 UNKNOWN,但不代表 PMS 丢失了状态。
case "hide-notifications":
flags |= PackageManager.RESTRICTION_HIDE_NOTIFICATIONS;
break;
case "hide-from-suggestions":
flags |= PackageManager.RESTRICTION_HIDE_FROM_SUGGESTIONS;
break;诊断组合限制时应同时读取原始 state 整数并按位解析,不要只依赖 shell 的单值字符串。
9. 与挂起的边界
分心限制和 suspension 都可能由 SUSPEND_APPS 权限管理,也都拒绝关键包,但它们的消费者不同。suspension 会影响应用整体可启动性并维护多个 suspender;distraction 只给通知、建议和启动确认提供提示位。解除 distraction 是一次 bitmask 清零,不能按某个挂起方撤销。
10. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| 设置命令返回失败包 | user restriction、关键包、visibility | 逐包结果在 helper 中收集 |
| state 为 3 但 shell 显示 UNKNOWN | stateToString | 组合 bit 没有单值文案 |
| 通知仍可见 | ACTION_DISTRACTING_PACKAGES_CHANGED、NMS 位判断 | NMS 只消费 0x2,且广播异步 |
| speedbump 不出现 | feature flag、Wellbeing package、Intent resolve | 0x4 只是请求确认,失败会放行原启动 |
| 重启后限制消失 | restrictions 写盘和 XML 读取 | 内存状态已变不代表持久化成功 |
| 清除限制被拒绝 | 是否使用 RESTRICTION_NONE | 清除路径不走新增限制的 user policy 拒绝 |
11. 源码练习
- 以
restrictionFlags=0x3追踪从 PMS mutation 到 NMS 的结果,指出 suggestions 位在哪个消费者生效、为什么 NMS 不处理它。 - 构造
0x4speedbump 但没有 Wellbeing handler 的设备状态,按ActivityStartInterceptor分支判断最终是否阻止启动。 - 对比设置
0x2和清除为0x0的 user restriction 检查,解释为什么策略阻止新增限制却允许恢复旧状态。 - 使用
pm get-distracting得到UNKNOWN时,回到整数 bitmask、SettingsXML 和 NMS/ActivityStartInterceptor 三个消费者分别验证状态。
