组件标签图标覆盖
本文面向已经读过 组件启用状态变更、PackageStateMutator 事务提交 和 PackageSetting 数据结构 的读者。本文不讨论资源 overlay 或 Manifest 默认 label/icon,而是解释系统如何为某个 user 的特定 Activity/Service/Provider/Receiver 保存非本地化 label 和 icon 覆盖,谁有权设置它,以及 Launcher/Resolver 何时看到新值。
1. 覆盖状态
源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java。
覆盖值是 per-user 的 component map,而不是 AndroidPackage 的全局字段:
private WatchedArrayMap<ComponentName,
Pair<String, Integer>> mComponentLabelIconOverrideMap;key 是完整 ComponentName,value 的 first 是非本地化 label,second 是 icon resource id;任意一项可以为 null。两项都为 null 表示删除该组件的覆盖条目并在 map 为空时释放整个 map。
2. 双重身份校验
源码文件:frameworks/base/services/core/java/com/android/server/pm/PackageManagerService.java;符号:updateComponentLabelIcon。
调用者必须同时满足两项:UID 与目标 component 所属 package 相同;UID 还必须属于资源 config_overrideComponentUiPackage 指定的允许包。少任何一项都抛 SecurityException。
源码文件:frameworks/base/services/core/java/com/android/server/pm/pkg/PackageUserStateImpl.java;符号:overrideLabelAndIcon。
if (!computer.isCallerSameApp(componentPkgName, callingUid)) {
throw new SecurityException("calling UID does not match target UID");
}
String allowedCallerPkg = mContext.getString(
R.string.config_overrideComponentUiPackage);
if (TextUtils.isEmpty(allowedCallerPkg)
|| !computer.isCallerSameApp(allowedCallerPkg, callingUid)) {
throw new SecurityException("caller is not allowed");
}这不是普通 CHANGE_COMPONENT_ENABLED_STATE 权限路径。设计意图是:只有目标系统应用自身、并且由 OEM 配置的 UI 包,才能改变组件展示信息;任意 system/privileged caller 都不能仅凭 UID 绕过配置。
3. 目标包限制
校验通过后,PMS 还要求 package state 存在、拥有 parsed package,且包是 system 或 updated-system app。第三方普通应用不能使用该 API 覆盖自己的 label/icon。组件必须在 ComponentResolver 中存在,否则抛 IllegalArgumentException。
PackageStateInternal packageState =
computer.getPackageStateInternal(componentPkgName);
if (packageState == null || packageState.getPkg() == null
|| (!packageState.isSystem()
&& !packageState.isUpdatedSystemApp())) {
throw new SecurityException(
"Changing the label is not allowed for " + componentName);
}
if (!computer.getComponentResolver().componentExists(componentName)) {
throw new IllegalArgumentException(
"Component " + componentName + " not found");
}组件存在性检查使用当前 snapshot;更新/卸载并发时,旧 snapshot 可能仍包含组件,真正写入仍在 PMS 状态锁内完成,调用者应把 broadcast 和新 snapshot 作为最终观察点。
4. 空值清除
PackageUserStateImpl.overrideLabelAndIcon 先读取旧 Pair,再比较 label/icon。相同值直接返回 false,不创建新的 map 变化;label 和 icon 同时为 null 时移除 key。
boolean changed = !TextUtils.equals(existingLabel,
nonLocalizedLabel) || !Objects.equals(existingIcon, icon);
if (changed) {
if (nonLocalizedLabel == null && icon == null) {
mComponentLabelIconOverrideMap.remove(component);
if (mComponentLabelIconOverrideMap.isEmpty()) {
mComponentLabelIconOverrideMap = null;
}
} else {
if (mComponentLabelIconOverrideMap == null) {
mComponentLabelIconOverrideMap = new WatchedArrayMap<>(1);
mComponentLabelIconOverrideMap.registerObserver(mSnapshot);
}
mComponentLabelIconOverrideMap.put(component,
Pair.create(nonLocalizedLabel, icon));
}
}因此 restoreLabelAndIcon(component, userId) 通过传入两个 null 实现恢复 Manifest 默认值;它不是写入空字符串或 icon id 0。只清 label、保留 icon,或只清 icon、保留 label,都会保留 map 条目。
5. 状态提交
PMS 使用指定 package 的 commitPackageStateMutation 写入 user state:
commitPackageStateMutation(null, componentPkgName,
state -> state.userState(userId)
.setComponentLabelIcon(componentName,
nonLocalizedLabel, icon));PackageStateMutator 的 setter 触发 PackageUserStateImpl snapshot watcher,随后 PMS 将组件加入 mPendingBroadcasts。因为这是 user state,user 0 的覆盖不会改变 user 10 的 label/icon。
6. 广播刷新
覆盖成功后不会立即对每个 receiver 发送独立广播,而是将组件加入 pending list,并安排 SEND_PENDING_BROADCAST。消息使用 PackageMetrics.STRING_COMPONENT_LABEL_ICON_CHANGED 作为原因;handler 执行时批量发送 package-changed 通知。
mPendingBroadcasts.addComponent(userId,
componentPkgName, componentName.getClassName());
if (!mHandler.hasMessages(SEND_PENDING_BROADCAST)) {
mHandler.sendMessageDelayed(
mHandler.obtainMessage(SEND_PENDING_BROADCAST,
callingUid, 0, STRING_COMPONENT_LABEL_ICON_CHANGED),
BROADCAST_DELAY);
}内存中的 resolver 查询可以先看到新覆盖,Launcher 等依赖广播刷新的缓存则要等 pending message。广播延迟不表示 mutation 失败。
7. 查询消费
PackageUserState 的 component label/icon override 会在生成 ActivityInfo、ResolveInfo 或 Launcher 相关信息时与 Manifest 值合并。覆盖只改变展示字段,不改变 component enabled、intent filter、权限或 package visibility。
因此“图标变了但组件仍不可启动”不是覆盖机制异常;应分别检查 enabled state、suspension、visibility 和启动 resolver。反之,广播未到达时,组件可能仍能启动但 Launcher 显示旧 label/icon。
8. 更新与清除
系统包更新路径可能调用 resetOverrideComponentLabelIcon(userId),避免新 APK 的 component 集合与旧 override map 不匹配。清除 app data 时,如果包声明 resetEnabledSettingsOnAppDataCleared,PMS 重置的是 enabled component overrides;label/icon override 的清理由各自的 restore API 或包状态重建路径决定,不能把两者视为同一集合。
PackageUserStateImpl 的 snapshot copy 对 override map 调用 snapshot();更新 map 会使 user-state snapshot 失效,旧 PackageInfo 不会自动被修改。
9. 持久化
override map 随 user package restrictions 写入 Settings。每个 component 的 label/icon pair 作为 user state 的嵌套数据保存;未设置覆盖的普通包不分配 map,也不会产生 XML 节点。
写盘由 scheduleWritePackageRestrictions(userId) 的异步消息完成。即使 updateComponentLabelIcon 返回成功,进程崩溃发生在 handler 写盘前,重启后也可能回到旧覆盖;广播和持久化是两个独立完成点。
10. 失败定位
| 现象 | 检查点 | 解释 |
|---|---|---|
| SecurityException caller UID | 目标 package UID、config_overrideComponentUiPackage | 需要目标应用与允许 UI 包的 UID 交集 |
| SecurityException 非系统包 | packageState.isSystem/isUpdatedSystemApp | 普通第三方包不能覆盖 |
| Unknown component | ComponentResolver snapshot | 类名不存在或更新期间 snapshot 过期 |
| 返回成功但 UI 仍旧 | mPendingBroadcasts 与 handler | 广播延迟,内存状态可能已经更新 |
| restore 后仍显示覆盖 | 两个 null 是否真正写入、userId 是否正确 | 清除是删除 map key,不是空字符串 |
| 重启后覆盖丢失 | restrictions 写盘时机 | mutation 成功不等于 XML 已落盘 |
| user 0 与 user 10 不同 | PackageUserState per-user map | 覆盖不是全局 AndroidPackage 字段 |
11. 源码练习
- 从
overrideLabelAndIcon追踪 UID 双重校验、system 包校验、componentExists、mutation 和 pending broadcast 的顺序。 - 构造只修改 icon、再传入两个 null 的两次调用,判断 map 中 Pair 如何变化以及何时释放 map。
- 对比内存 resolver 与 Launcher 缓存,说明为什么 mutation 成功后两者看到新 label/icon 的时刻不同。
- 给定 user 0 覆盖成功、user 10 查询仍是 Manifest 值,沿
PackageUserState、snapshot 和 restrictions 文件验证这是预期的用户隔离还是写入错误。
