Skip to content

分心限制机制

追踪 Android 17 分心限制的 bitmask 写入、关键包过滤、广播以及通知和启动消费者。

基于android-17.0.0_r1
AndroidPMSDistractingPackageHelperNotificationManagerActivityTaskManager

分心限制机制 ​

本文承接 包挂起与恢复 和 PackageStateMutator 事务提交,讨论另一套容易与 suspension 混淆的 per-user 状态:distraction restrictions。它不把应用整体挂起,而是用 bitmask 分别影响启动建议、通知展示以及带确认页的启动。读完后,读者应能从 Binder 入口追到 PackageUserStateImpl.mDistractionFlags,再分别定位 NotificationManagerService 和 ActivityTaskManager 的消费者。

1. 三个位 ​

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

Android 17 定义三个可组合标志:

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;

0x1 是从推荐中隐藏,0x2 才是隐藏通知。三个值由 @IntDef(flag = true) 声明,可以按位组合,例如 0x3 同时影响推荐和通知。RESTRICTION_NONE 用于清除全部限制,不是“保留原标志不变”。

2. 状态 owner ​

源码文件:

  • frameworks/base/services/core/java/com/android/server/pm/DistractingPackageHelper.java
  • frameworks/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。

java
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,不会被这条用户限制阻止。

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

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

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 位。

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

java
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 restrictionSuspension
状态结构单个 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、目标 userNMS 通过异步广播消费新位
speedbump 不出现feature flag、Wellbeing package、Intent resolveflag 不保证拦截一定发生
shell 显示 UNKNOWN多位组合不是单一 switch case应按位解析数值而非依赖字符串

13. 源码练习 ​

  1. 从 setDistractingPackageRestrictionsAsUser 追到 NMS,标出权限、关键包检查、mutation、广播和通知隐藏的五个时点。
  2. 以旧值 0x1、新值 0x2 为输入,说明为什么结果不是 0x3,以及调用方怎样保留原限制。
  3. 给定 0x4 已写入但启动没有确认页,按 feature flag、Wellbeing UID、Intent resolve 三个分支定位。
  4. 对比 distraction 和 suspension 的解除流程,说明为什么前者不能按某个管理者单独撤销。