Skip to content

分心限制消费者

追踪 Android 17 分心限制从 bitmask 写入到通知、建议和启动确认消费者的实际生效路径。

基于android-17.0.0_r1
AndroidPMSDistractingPackageHelperNotificationManagerActivityStartInterceptor

分心限制消费者 ​

本文承接 分心限制机制 和 包挂起与恢复。上一篇讲状态如何写入 PMS;本文继续追踪状态离开 PMS 后由谁消费、何时生效,以及为什么同一个 restriction bitmask 会在不同服务中产生不同结果。文章不讨论 Digital Wellbeing 如何决定策略,只讨论 Android 17 中已经确定的执行链。

1. Bitmask 语义 ​

源码文件:frameworks/base/core/java/android/content/pm/PackageManager.java。

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 替换为新值。

java
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,因此这是可逆的隐藏状态,不是删除通知记录。

java
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 才继续。

java
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:

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 丢失了状态。

java
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 显示 UNKNOWNstateToString组合 bit 没有单值文案
通知仍可见ACTION_DISTRACTING_PACKAGES_CHANGED、NMS 位判断NMS 只消费 0x2,且广播异步
speedbump 不出现feature flag、Wellbeing package、Intent resolve0x4 只是请求确认,失败会放行原启动
重启后限制消失restrictions 写盘和 XML 读取内存状态已变不代表持久化成功
清除限制被拒绝是否使用 RESTRICTION_NONE清除路径不走新增限制的 user policy 拒绝

11. 源码练习 ​

  1. 以 restrictionFlags=0x3 追踪从 PMS mutation 到 NMS 的结果,指出 suggestions 位在哪个消费者生效、为什么 NMS 不处理它。
  2. 构造 0x4 speedbump 但没有 Wellbeing handler 的设备状态,按 ActivityStartInterceptor 分支判断最终是否阻止启动。
  3. 对比设置 0x2 和清除为 0x0 的 user restriction 检查,解释为什么策略阻止新增限制却允许恢复旧状态。
  4. 使用 pm get-distracting 得到 UNKNOWN 时,回到整数 bitmask、Settings XML 和 NMS/ActivityStartInterceptor 三个消费者分别验证状态。