原生设置入口显隐:config 与 RRO overlay 定制
用 config 或 overlay 控制原生设置页面的入口显隐
入口显隐没有全局总开关
“设置入口显示隐藏”没有全局总开关。每一个入口都有自己的 PreferenceController,由 getAvailabilityStatus() 决定显示、置灰还是隐藏。其中一部分直接读 R.bool.config_show_*;另一部分读硬件 feature、Settings.Global/Secure 或运行时状态。 所以定制前第一件事是找到入口对应的 controller,再决定改哪里。
核心结论
- AOSP Settings 的
res/values/config.xml里有一批config_show_*布尔资源,直接控制 首页入口和二级页面的显隐,例如config_show_top_level_battery、config_show_manual。 - 改资源值有两条路:直接改
packages/apps/Settings源码(厂商有源码时最直接);或做 RRO overlay 包(不碰 Settings 源码,Android 10+ 推荐,Android 14 仍是标准做法)。 - AOSP Settings 没有声明
<overlayable>资源组。按 idmap2 的规则,未声明 overlayable 的目标包允许覆盖其全部资源,因此 RRO 不需要android:targetName。 - 入口隐藏后,搜索索引会同步失效:
BaseSearchIndexProvider会把不可用入口的 key 加进 non-indexable 列表,避免“页面里没有、搜索还能搜到”。
主流程:从配置文件到入口消失
页面渲染侧,入口由 controller 的 availability 决定:
RRO overlay 侧,资源在运行时被替换:
Settings 的 config.xml 里到底有什么
以下条目同时存在于 Android 14(android-14.0.0_r75)和 AOSP main 分支的 packages/apps/Settings/res/values/config.xml:
<!-- packages/apps/Settings/res/values/config.xml(节选,值为 Android 14 默认值) -->
<bool name="config_show_top_level_battery">true</bool>
<bool name="config_show_top_level_display">true</bool>
<bool name="config_show_top_level_accessibility">true</bool>
<bool name="config_show_top_level_connected_devices">true</bool>
<bool name="config_show_device_model">true</bool>
<bool name="config_show_device_name">true</bool>
<bool name="config_show_manual">false</bool>
<bool name="config_show_regulatory_info">false</bool>
<bool name="config_show_wifi_settings">true</bool>
<bool name="config_show_sim_info">true</bool>注意:条目随版本变化。例如 config_show_internet_settings 是 AOSP main 新增的条目, Android 14 里并不存在。定制前务必以目标版本的源码为准,不要照抄别的版本的条目名。
Java 侧怎么读
典型读取方式是 controller 在 getAvailabilityStatus() 里读资源布尔:
// TopLevelBatteryPreferenceController.getAvailabilityStatus()
return mContext.getResources().getBoolean(R.bool.config_show_top_level_battery)
? AVAILABLE
: UNSUPPORTED_ON_DEVICE;“关于手机”页的头部则由 Fragment 直接控制:
// MyDeviceInfoFragment.initHeader()
final boolean shouldDisplayHeader = getContext().getResources().getBoolean(
R.bool.config_show_device_header_in_device_info);
headerPreference.setVisible(shouldDisplayHeader);一个关键点:controller 的 availability 同时驱动两个消费方——页面渲染和搜索索引。 BasePreferenceController.updateNonIndexableKeys() 会把不可用入口的 preference key 加进 non-indexable 列表。所以“入口隐藏了但搜索还能搜到”通常是该入口没有走 controller, 而是由 XML 写死或由另一套索引逻辑提供。
方案 A:直接改 Settings 源码
适合手里有 AOSP 源码、只维护一两个产品线的场景:
- 先找到目标入口对应的资源:
rg "config_show_top_level_battery" packages/apps/Settings。 - 修改
res/values/config.xml里的布尔值。 - 重新编译:
m Settings(或包含 Settings 的整包)。 - 验证:打开设置首页,目标入口消失。
局限:每个产品线都要维护 Settings 源码分支,升级 AOSP 版本时容易冲突;如果同一套源码 要出多个版本(例如运营商定制、不同 SKU),改源码意味着每个版本各维护一份。
方案 B:RRO overlay(推荐,不碰 Settings 源码)
RRO(Runtime Resource Overlay)是独立 APK,运行时通过 idmap 把目标包的部分资源替换成 overlay 的值。下面以一个隐藏“首页电池入口”的 overlay 为例。
1. 建目录
device/<oem>/<product>/overlay/SettingsOverlay/
├── Android.bp
├── AndroidManifest.xml
└── res/values/config.xml2. AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.overlay.settings">
<application android:hasCode="false" />
<overlay
android:targetPackage="com.android.settings"
android:isStatic="true" />
</manifest>要点:
targetPackage必须精确等于com.android.settings,拼错会得到MISSING_TARGET。- Settings 没有声明 overlayable,所以不要写
android:targetName。Android 10+ 中,如果目标 声明了 overlayable,而 overlay 的targetName写错或资源不属于任何组,idmap 生成会失败。 android:isStatic="true"表示开机默认启用且不可变。Android 14 的OverlayConfig(com.android.internal.content.om.OverlayConfig)会把静态 overlay 转成 “不可变 + 默认启用”的配置;该属性已被标记为逐步弃用,新项目建议用 overlay config 配置文件 (partition/overlay/config/config.xml)声明 enabled、mutable 和 priority。android:category和android:priority只在多个 overlay 作用于同一 target 时影响排序, 只有一个 overlay 时可以省略。
3. res/values/config.xml(与目标同名同类型)
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- 隐藏首页“电池”入口 -->
<bool name="config_show_top_level_battery">false</bool>
<!-- 隐藏“关于手机”头部(实体头像/名称卡片) -->
<bool name="config_show_device_header_in_device_info">false</bool>
</resources>注意:overlay 里写的资源名、类型必须与目标 APK 完全一致;overlay 只能覆盖目标已存在的 资源 ID,不能凭空新增目标没有的资源。
4. Android.bp 与编译
android_app {
name: "SettingsOverlay",
certificate: "platform",
srcs: ["AndroidManifest.xml"],
resource_dirs: ["res"],
sdk_version: "current",
}- 在设备产品配置中预装:
PRODUCT_PACKAGES += SettingsOverlay。overlay 必须落在/system、/system_ext、/product、/vendor或/odm等系统分区;装在数据分区的包 不会被当作可启用的 overlay。 certificate: "platform"与 Settings 同签名,会额外满足 idmap2 的SIGNATUREpolicy。 注意:对未声明 overlayable 的 Settings,policy 不拦截覆盖;但目标若声明了 overlayable, 签名和分区会决定能覆盖哪些资源。
5. 验证
# 主机终端;需要包含 SettingsOverlay 的 userdebug/eng 构建
m SettingsOverlay
# 设备端查看 overlay 状态
adb shell cmd overlay list --user 0
adb shell dumpsys overlay预期:cmd overlay list 中该 overlay 显示为启用状态,Settings 进程自动重启后入口消失。 overlay 状态变化后,系统会经 PMS→AMS→进程链路向目标包广播 ACTION_OVERLAY_CHANGED 并 下发新的 ApplicationInfo,Settings 进程会自动重启以重载资源,不需要手动 am force-stop。
不是 config 控制的入口怎么办
只有 controller 读 config_show_* 的入口能用上面两招。其余入口的常见机制:
- 硬件 feature:NFC、蓝牙等入口大多由
PackageManager.hasSystemFeature()决定, 隐藏它们要改产品特性声明,不是改 Settings 资源。 Settings.Global/Secure:开发者选项由Settings.Global.DEVELOPMENT_SETTINGS_ENABLED控制,可执行adb shell settings put global development_settings_enabled 0关闭。这是 运行时手段,适合调试验证,不适合作为编译期定制。- 组件级禁用:
adb shell pm disable-user --user 0 com.android.settings/.Settings$XxxActivity适合快速验证某个入口是否由独立 Activity 承载。注意它禁用的是组件,不限于入口可见性。
判断方法:在 Settings 源码里搜目标入口的 preference key,找到它注册在哪个 Fragment/XML, 再看对应 controller 的 getAvailabilityStatus() 读了什么。
验证与排障
环境:调试设备 + 主机终端;overlay 状态操作需要 root 或 shell 的 overlay 权限 (userdebug/eng 构建默认具备)。
| 命令 | 预期与用途 |
|---|---|
adb shell cmd overlay list --user 0 | 看到 overlay 及其 [x]/[ ] 启用状态 |
adb shell dumpsys overlay | 查看目标包、enabled、状态(ENABLED/NO_IDMAP/MISSING_TARGET) |
adb shell cmd overlay enable --user 0 <overlay包名> | 手工启用(isStatic/overlay config 场景通常无需) |
adb shell cmd overlay disable --user 0 <overlay包名> | 临时关闭,便于对比 |
adb logcat -s idmap2 OverlayManagerSettings | 看 idmap 生成失败的报错 |
常见失败:
- 状态为
NO_IDMAP:overlay 与目标资源映射失败。优先检查资源名/类型是否一致、overlay 是否 预装在系统分区、以及targetName是否写错(Android 10+ 对声明了 overlayable 的目标会拒绝)。 - 状态为
MISSING_TARGET:com.android.settings不存在或拼写错误。 - overlay 已启用但入口没消失:该入口不读
config_show_*,回到 controller 源码查它读什么。
常见误区
- “overlay 必须和目标包同签名”:不完全对。对未声明 overlayable 的目标包,idmap2 的 policy 不拦截任何资源;对声明了 overlayable 的目标包,
PUBLICpolicy 恒满足,签名只是 额外满足SIGNATUREpolicy。硬性要求是 overlay 预装在系统分区并能被 OverlayManagerService 识别。 PRODUCT_PACKAGE_OVERLAYS和 RRO 是同一件事:不是。前者是编译期把设备树资源直接 合进目标包的旧机制,Android 10 起官方建议改用 RRO;RRO 是独立 APK + idmap 运行时映射。- Android 14 还能不能用
android:isStatic:能用,系统仍会把它转成“不可变 + 默认启用”, 但官方已把该属性标记为逐步弃用;新项目优先用 overlay config。 - 隐藏入口就万事大吉:不一定。若入口还注册了快捷方式、Activity 别名或 Slice,隐藏页面 入口后这些路径仍可能可达,需要按业务边界决定是否同时禁用组件。
- 改 config.xml 就能隐藏一切:不能。
config_show_*只覆盖由资源布尔控制的入口,且条目 随版本变化,例如config_show_internet_settings是 AOSP main 才有的条目。
延伸问题
- 如何让 overlay 只在特定 SKU 生效?用
android:requiredSystemPropertyName和android:requiredSystemPropertyValue绑定系统属性,不满足时 overlay 直接不启用。 - 多个 overlay 叠加时顺序怎么定?
priority+ category 排序;Android 14 的 overlay config 还支持 partition 顺序(partition_order.xml)。 - 搜索里为什么还能搜到隐藏入口?检查
BaseSearchIndexProvider是否经过 controller 的updateNonIndexableKeys(),或入口 key 是否被硬编码在 XML。 - 厂商给 Settings 声明了 overlayable 后,RRO 为什么突然失效?因为
targetName必须匹配声明 中的组名,且资源必须属于该组。AOSP Settings 未声明 overlayable,反而让覆盖变得简单。
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
packages/apps/Settings/res/values/config.xml | config_show_* | 入口显隐资源定义 |
packages/apps/Settings/src/com/android/settings/fuelgauge/TopLevelBatteryPreferenceController.java | getAvailabilityStatus() | 首页“电池”入口显隐 |
packages/apps/Settings/src/com/android/settings/display/TopLevelDisplayPreferenceController.java | getAvailabilityStatus() | 首页“显示”入口显隐 |
packages/apps/Settings/src/com/android/settings/accessibility/TopLevelAccessibilityPreferenceController.java | getAvailabilityStatus() | 首页“无障碍”入口显隐 |
packages/apps/Settings/src/com/android/settings/connecteddevice/TopLevelConnectedDevicesPreferenceController.java | getAvailabilityStatus() | 首页“已连接的设备”入口显隐 |
packages/apps/Settings/src/com/android/settings/deviceinfo/aboutphone/MyDeviceInfoFragment.java | initHeader() | “关于手机”头部显隐 |
packages/apps/Settings/src/com/android/settings/deviceinfo/ManualPreferenceController.java | getAvailabilityStatus() | “手册”入口显隐 |
packages/apps/Settings/src/com/android/settings/search/BaseSearchIndexProvider.java | getNonIndexableKeys() | 搜索索引过滤不可用入口 |
packages/apps/Settings/src/com/android/settings/core/BasePreferenceController.java | updateNonIndexableKeys() | availability 状态映射到索引键 |
frameworks/base/cmds/idmap2/libidmap2/ResourceMapping.cpp | CheckOverlayable() | overlayable 与 policy 校验 |
frameworks/base/services/core/java/com/android/server/om/IdmapManager.java | calculateFulfilledPolicies() | overlay policy 计算 |
frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java | updateState() | overlay 状态机 |
frameworks/base/core/java/com/android/internal/content/om/OverlayConfig.java | 静态 overlay 转换 | Android 14 默认启用/不可变语义 |