Skip to content

组件启用状态变更

追踪 Android 17 组件启用状态的权限校验、批量提交、广播调度和用户状态持久化。

基于android-17.0.0_r1
AndroidPMSComponentStatePackageUserStateBroadcast

组件启用状态变更 ​

本文面向已经读过 PackageStateMutator 事务提交、PackageSetting 数据结构 和 ComponentResolver 组件解析器 的读者。它只讨论 setComponentEnabledSetting/setComponentEnabledSettings 如何修改 per-user enabled state,以及状态变化怎样影响解析和广播;不重复讲 pm 命令语法,也不把 package enabled state 和 stopped state 混为一谈。

1. 入口与对象 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java。

Binder 层有单项和批量两个入口,最终都汇入私有的 setEnabledSettings:

java
public void setComponentEnabledSetting(ComponentName componentName,
        int newState, int flags, int userId, String callingPackage) {
    if (!mUserManager.exists(userId)) return;
    if (callingPackage == null) {
        callingPackage = Integer.toString(Binder.getCallingUid());
    }
    setEnabledSettings(List.of(new ComponentEnabledSetting(
            componentName, newState, flags)), userId, callingPackage);
}

输入对象 ComponentEnabledSetting 可以代表整个 package,也可以代表一个具体 component。目标状态最终写入 PackageSetting 的用户状态:package 级别写 mEnabledState,component 级别写 enabled/disabled component 集合。

2. 输入校验 ​

源码符号:setEnabledSettings。

首先只接受五种状态:DEFAULT、ENABLED、DISABLED、DISABLED_USER 和 DISABLED_UNTIL_USED。批量请求还禁止重复 package、重复 component,以及同一 package 内 DONT_KILL_APP 标志冲突。

java
if (!(newState == COMPONENT_ENABLED_STATE_DEFAULT
        || newState == COMPONENT_ENABLED_STATE_ENABLED
        || newState == COMPONENT_ENABLED_STATE_DISABLED
        || newState == COMPONENT_ENABLED_STATE_DISABLED_USER
        || newState == COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED)) {
    throw new IllegalArgumentException(
            "Invalid new component state: " + newState);
}

重复和 flag 冲突在任何状态写入前抛出异常,因此批量列表不会出现“前几个已经改了,后几个才失败”的部分提交。

3. 调用者与目标 ​

源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:setEnabledSettings。

入口先用 enforceCrossUserPermission 校验 user,再在 mLock 内建立 snapshot 检查每个目标包。调用者是目标 app 自身时可以修改自己的组件;修改其他包则必须持有 CHANGE_COMPONENT_ENABLED_STATE。

java
boolean isCallerTargetApp = ArrayUtils.contains(
        snapshot.getPackagesForUid(callingUid), packageName);
if (!isCallerTargetApp && !allowedByPermission) {
    throw new SecurityException("Attempt to change component state");
}
if (pkgSetting == null || snapshot.shouldFilterApplicationIncludingUninstalled(
        pkgSetting, callingUid, userId)) {
    throw new IllegalArgumentException("Unknown package/component");
}

可见性检查发生在 protected package 检查之前。对调用者不可见的包会得到 unknown,而不会暴露它是否存在、是否是 device owner 或是否受保护。

4. 受保护包 ​

非目标 app 修改 package state 时,PMS 调用 mProtectedPackages.isPackageStateProtected(userId, packageName)。命中 device/profile owner、DPC、provisioning 或 supervision 保护时抛 SecurityException。启用 protectSystemRequiredPackages() 后,required system package 的 package 级禁用还要额外检查 ALLOW_CONTROL_SYSTEM_REQUIRED_PACKAGES。

组件级请求另有一个系统生成的 app details activity 限制:没有 CHANGE_COMPONENT_ENABLED_STATE 时不能禁用 APP_DETAILS_ACTIVITY_CLASS_NAME。这和 protected package 是两条独立规则。

5. 组件存在性 ​

每个 component 请求都会通过 AndroidPackageUtils.hasComponentClassName 验证 class name。targetSdk >= Jelly Bean 的包,类不存在会抛 IllegalArgumentException;旧 targetSdk 则只记录 warning,并把对应 updateAllowed 设为 false,让批量请求继续处理其他合法项。

这种兼容分支意味着“方法返回成功”不一定表示列表中的每个旧 target component 都改变了状态。调试批量请求时要同时看异常、warning 和 updateAllowed。

6. 状态写入 ​

源码符号:setEnabledSettingInternalLocked。

java
if (!setting.isComponent()) {
    pkgSetting.setEnabled(newState, userId, callingPackage);
    success = true;
} else {
    switch (newState) {
        case COMPONENT_ENABLED_STATE_ENABLED:
            success = pkgSetting.enableComponentLPw(className, userId);
            break;
        case COMPONENT_ENABLED_STATE_DISABLED:
            success = pkgSetting.disableComponentLPw(className, userId);
            break;
        case COMPONENT_ENABLED_STATE_DEFAULT:
            success = pkgSetting.restoreComponentLPw(className, userId);
            break;
    }
}

package 级 DISABLED_UNTIL_USED 等状态由 setEnabled 保存;component 级只接受 enabled/disabled/default,DISABLED_USER 不会进入 component switch。写入后调用 updateSequenceNumberLP,让 resolver 和查询 snapshot 知道该 package/user 的状态发生变化。

7. 进程与广播 ​

每项设置的 flags 会影响两个独立行为:是否杀进程,以及 PACKAGE_CHANGED 广播何时发送。没有 DONT_KILL_APP 的项加入 sendNowBroadcasts,退出锁后立即发送;带 DONT_KILL_APP 的项进入 mPendingBroadcasts,由 handler 延迟批量发送。

批量请求中同一 package 的多个 component 若混用 DONT_KILL_APP 会在前置校验阶段拒绝,避免一部分组件立即广播、另一部分组件延迟广播。

8. 持久化时机 ​

如果任何 setting 带 SYNCHRONOUS,PMS 在 mLock 内调用 flushPackageRestrictionsAsUserInternalLocked;否则调用 scheduleWritePackageRestrictions(userId)。同步 flag 只控制 restrictions 文件写盘,不改变组件状态的内存提交顺序。

退出锁后,PMS 生成新的 snapshot 给 mBroadcastHelper.sendPackageChangedBroadcast;metrics 则使用写入前的旧 snapshot 计算 old state。这是有意的观察点:指标要记录“从什么状态变到什么状态”,广播消费者要看到新状态。

9. 特殊副作用 ​

package 级禁用 SUSPEND_APPS 权限持有者时,PMS 会解除该包作为 suspender 造成的挂起,并移除 distracting package restrictions,避免禁用一个管理包后其他应用永久 suspended。

启用 system stub package 时,PMS 还可能先解压压缩 APK 并替换 system 分区版本;解压失败只把该项 updateAllowed 置为 false,不会写入 enabled state。清除 app data 时,如果 manifest 的 resetEnabledSettingsOnAppDataCleared 为 true,PMS 会恢复 Activity、Receiver、Service、Provider 的 component overrides 到 default。

10. 查询消费者 ​

组件解析器读取 PackageUserState 的 enabled state,再结合 manifest component 的默认 enabled 属性决定是否产生 ResolveInfo。COMPONENT_ENABLED_STATE_DEFAULT 并不等于强制启用,而是回退到 manifest;MATCH_DISABLED_COMPONENTS 等查询 flag 可以改变结果过滤,但不会修改持久状态。

因此排查“组件已 enable 但仍解析不到”时,应分开检查:写入是否成功、目标 user 是否正确、resolver 使用的 flags、组件自身 manifest enabled,以及 pending broadcast 是否已经发送。广播延迟不会改变 resolver 已提交的内存状态,但会影响依赖 PACKAGE_CHANGED 刷新的外部缓存。

11. 失败定位 ​

现象检查源码解释
抛 Invalid new component statesetEnabledSettings 状态白名单输入值不是五种公开状态之一
Unknown componentvisibility、user、class name 校验可能不可见、未安装或类名不存在
protected package SecurityExceptionProtectedPackages 与 required system 检查system/privileged 不自动绕过保护
批量请求部分无效updateAllowed、旧 targetSdk 分支warning 路径会跳过单项而不抛异常
进程未重启DONT_KILL_APP flags该 flag 只延迟广播并保留进程
状态已变但外部缓存未刷新pending broadcast handler内存提交、restrictions 写盘、广播是三个时点
清数据后组件恢复默认resetComponentEnabledSettingsIfNeededLPwmanifest flag 明确要求清理 enabled overrides

12. 源码练习 ​

  1. 从单项 Binder 入口追到 setEnabledSettingInternalLocked,标出 user 权限、可见性、protected、组件存在性和真正 setter 的顺序。
  2. 构造同一 package 两个 component 一个带 DONT_KILL_APP、一个不带的批量请求,指出异常发生在写入前的哪一步。
  3. 对比 package 级 DEFAULT 和 component 级 DEFAULT,说明它们如何分别回退到 package/manifest 的默认 enabled 语义。
  4. 给定 resolver 已看到新状态但第三方缓存仍旧,沿 sendNowBroadcasts、mPendingBroadcasts 和 package restrictions 写盘三个时间点定位原因。