03 - Flavor / Region 裁剪机制(XCD cn 看不到「系统语言」入口的根因)
知识库(语言切换)第 3 篇。本篇回答一个具体高频疑问:
「为什么 XCD 国内版(XCD cn)打开 设置 → 系统,找不到『系统语言』入口?」
一句话结论先放在这里:「系统语言」入口的显隐与 Flavor(dcd/xcd/global)无关,由运行时读取的 SystemProperty
ro.micar.build.region决定。国内版该字段值为cn,触发UniversalSettingsFragment.getPreferenceKeyResIdsToRemove()移除逻辑,入口被运行时裁剪。 Flavor(编译期)与 Region(运行期)是两个正交维度。
0. 关键事实速查(直接源码核对)
| 名称 | 实际值 / 含义 | 源码位置 |
|---|---|---|
| Region SystemProperty Key | ro.micar.build.region | base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/DeviceUtil.java:29 |
BUILD_REGION | SystemProperties.get("ro.micar.build.region") | DeviceUtil.java:29 |
BUILD_REGION_CN | "cn" | DeviceUtil.java:34 |
BUILD_REGION_GLOBAL | "global" | DeviceUtil.java:39 |
BUILD_REGION_EU | "eu" | DeviceUtil.java:44 |
ROM_BUILD_REGION | "ro.micar.build.region"(与 DeviceUtil 同一个 prop key) | base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/views/MiCarSettings.java:179 |
ECE_REGION | "ece" | MiCarSettings.java:180 |
关键发现:
DeviceUtil.BUILD_REGION与MiCarSettings.ROM_BUILD_REGION读取的是同一个 SystemPropertyro.micar.build.region,只是被两个类各自定义了一遍。所以一个 prop 决定两套裁剪逻辑:
DeviceUtil那一套:把 region 值映射成cn / global / eu / 海外 / 国内五个布尔判定;MiCarSettings.isEce()那一套:把 region 值与字符串"ece"比对,决定是否切换到 ECE 精简 XML。两个类没有共享常量,是历史遗留的重复定义(可能后续重构可合并)。
1. 两套 XML:主版本 vs ECE 精简版
UniversalSettingsFragment.getPreferenceScreenResId() 根据 MiCarSettings.isEce() 在两套 PreferenceScreen XML 之间二选一:
// settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/UniversalSettingsFragment.java:59-67
@Override
@XmlRes
public int getPreferenceScreenResId() {
if (MiCarSettings.isEce()) {
return R.xml.miauto_universal_settings_fragment_ece;
} else {
return R.xml.miauto_universal_settings_fragment;
}
}1.1 XML preference 清单三栏对比表
pk_* 为 preference key,标题列直接抄自 settingsPage/settingsSystem/src/main/res/values/strings.xml(标注 string name)。
| Preference | 主版本 miauto_universal_settings_fragment.xml | ECE 版 miauto_universal_settings_fragment_ece.xml | string name(标题) |
|---|---|---|---|
| 系统版本(SystemInfoPreference) | ✓ | ✓(注意:用的是 miauto.universal.SystemInfoPreferenceController,不同包路径) | os_system_version = “System version” |
| 车辆名称(VehicleNamePreferenceController) | ✓ | ✗ | os_vehicle_name = “Vehicle name” |
| 系统语言(SystemLanguagePreferenceController) | ✓ | ✗ | system_universal_language_title = “System language” |
| 时间格式(TimeFormatPreferenceController) | ✓ | ✗ | settings_system_time_format_title = “Time format” |
| 续航显示方式(EnergyBatteryLifeDisplayStyleController) | ✓ | ✗ | settings_system_battery_life_display_title = “Range display” |
| 距离单位 km/mi(DistanceUnitTabPrefController) | ✓ | ✗ | settings_system_distance_unit_title = “Distance unit” |
| 胎压单位(PressureUnitTabLayPrefController) | ✓ | ✗ | settings_system_pressure_unit_title = “Tire pressure unit” |
| 温度单位 ℃/℉(TemperatureUnitTabPrefController) | ✓ | ✗ | settings_system_temperature_unit_title = “Temperature unit” |
| 应用管理(AppManagerPrefController) | ✓ | ✗ | app_manager = “Manage apps” |
| 存储(MiStoragePrefController) | ✓ | ✗ | app_storage = “Storage” |
| 关于 — 参数 & 信息(AboutSettingsPreferenceController) | ✓ | ✓ | app_android_version = “Parameters & info” |
| 隐私声明(VehicalInternetSwitchPrefController) | ✓ | ✗ | privacy_declaration |
| 开发者选项(DeveloperOptionsEntryPreferenceController) | ✓ | ✓ | developer_options_settings |
| 恢复出厂设置(FactoryResetActivity intent) | ✓ | ✗ | os_factory_reset_title = “Factory reset” |
ECE 版本只有 3 个真正的 Preference(系统版本 / 关于-参数信息 / 开发者选项)+ SpacePreference 间距占位,其他全部物理裁掉(XML 里压根没有)。这种是编译期+运行期都看不到——XML 物理不含。
2. Region 判定决策树
DeviceUtil 把 ro.micar.build.region 的值映射成几个布尔函数(DeviceUtil.java:104-133):
// DeviceUtil.java:104-133
public static boolean isGlobalRegion() { return BUILD_REGION_GLOBAL.equals(BUILD_REGION); } // "global"
public static boolean isEURegion() { return BUILD_REGION_EU.equals(BUILD_REGION); } // "eu"
public static boolean isOverseasRegion() { return !isCnRegion(); }
public static boolean isCnRegion() {
return TextUtils.isEmpty(BUILD_REGION) || BUILD_REGION_CN.equals(BUILD_REGION); // "" 或 "cn"
}MiCarSettings.isEce() 单独再判一次:
// MiCarSettings.java:407-411
public static boolean isEce() {
String region = SystemProperties.get(ROM_BUILD_REGION, "");
MLog.i("isEce() region=" + region);
return TextUtils.equals(region, ECE_REGION); // "ece"
}2.1 Region 决策树(mermaid)
flowchart TD Start["读取 prop<br/><code>adb shell getprop ro.micar.build.region</code>"] --> CheckECE{"region == 'ece' ?<br/>(MiCarSettings.isEce)"} CheckECE -- "是 → isEce() = true" --> ECE_XML["加载 ECE 精简 XML<br/>miauto_universal_settings_fragment_ece.xml<br/>只含 3 个 Preference + 间距占位"] CheckECE -- "否 → isEce() = false" --> CheckCN{"region 为空或 == 'cn' ?<br/>(DeviceUtil.isCnRegion)"} CheckCN -- "是 → isCnRegion=true<br/>isOverseasRegion=false" --> CN_BRANCH["国内分支<br/>加载主 XML<br/>+ 运行时移除 language / distance / temperature 三项"] CheckCN -- "否 → isOverseasRegion=true" --> CheckEU{"region == 'eu' ?"} CheckEU -- "是 → isEURegion=true" --> EU_BRANCH["欧洲分支<br/>加载主 XML<br/>+ 三项保留<br/>(language/distance/temperature 全部显示)"] CheckEU -- "否 → 进入 GLOBAL_BRANCH" --> GLOBAL_BRANCH["海外其他分支<br/>含 isGlobalRegion=true 的 'global'<br/>+ 其他未识别值<br/>加载主 XML + 三项保留<br/>※ isGlobalRegion() 仅 region=='global' 时为 true"] CN_BRANCH --> Result1["❌ 看不到『系统语言』入口<br/>(这就是 XCD cn 的情况)"] EU_BRANCH --> Result2["✅ 显示『系统语言』入口"] GLOBAL_BRANCH --> Result3["✅ 显示『系统语言』入口"] ECE_XML --> Result4["❌ 看不到『系统语言』入口<br/>(XML 物理裁剪)"] classDef cn fill:#ffe4e4,stroke:#c00; classDef ok fill:#e4ffe4,stroke:#0a0; classDef warn fill:#fff4e4,stroke:#c80; class CN_BRANCH,Result1 cn; class EU_BRANCH,GLOBAL_BRANCH,Result2,Result3 ok; class ECE_XML,Result4 warn;
核心结论:
isOverseasRegion()的判定不看 region 值是不是"global",而是看「不是国内」(!isCnRegion())。也就是说:region ="global"/"eu"/"ece"/ 任何非"cn"非" "的值,都会被算作”海外”。但isEce()的判定优先级更高(在getPreferenceScreenResId()入口就拦住走 ECE XML),所以 ECE 走的是另一条物理裁剪路径。
3. XCD cn 看不到「系统语言」入口——逐步定位
3.1 移除逻辑(mermaid)
flowchart TD A["UniversalSettingsFragment.getPreferenceKeyResIdsToRemove()<br/>settingsPage/settingsSystem/.../UniversalSettingsFragment.java:69-80"] --> B{"!isOverseasRegion() ?<br/>(等价于 isCnRegion())"} B -- "是(国内)" --> C["打印日志:<br/>'Don't show system language preference,<br/>because not global region'"] C --> D["ids.add(R.string.pk_settings_system_universal_language)<br/>→ 移除『系统语言』"] C --> E["ids.add(R.string.pk_settings_system_distance_unit_entry)<br/>→ 移除『距离单位 km/mi』"] C --> F["ids.add(R.string.pk_settings_system_temperature_unit_entry)<br/>→ 移除『温度单位 ℃/℉』"] D --> G["返回 ids 给父类<br/>BasePreferenceFragment 根据 pk 移除对应 Preference"] E --> G F --> G B -- "否(海外)" --> H["返回空 ids<br/>三项都保留显示"] style B fill:#fff4e4,stroke:#c80 style D fill:#ffe4e4,stroke:#c00 style E fill:#ffe4e4,stroke:#c00 style F fill:#ffe4e4,stroke:#c00
源码片段(UniversalSettingsFragment.java:69-80):
@Override
public List<Integer> getPreferenceKeyResIdsToRemove() {
List<Integer> ids = new ArrayList<>();
if (!DeviceUtil.isOverseasRegion()) {
MLog.tag(TAG).i("Don't show system language preference, because not global region");
ids.add(R.string.pk_settings_system_universal_language);
// 非海外车型,不展示距离和温度单位
ids.add(R.string.pk_settings_system_distance_unit_entry);
ids.add(R.string.pk_settings_system_temperature_unit_entry);
}
return ids;
}3.2 跑一遍 XCD cn 的实际流程
| 步骤 | 代码点 | 实际取值 / 行为 |
|---|---|---|
| 1. 系统启动时 | DeviceUtil.java:29 静态字段初始化 | BUILD_REGION = SystemProperties.get("ro.micar.build.region") = "cn" |
| 2. 进入「设置 → 系统」 | UniversalSettingsFragment.getPreferenceScreenResId() | MiCarSettings.isEce() = TextUtils.equals("cn", "ece") = false → 返回主 XML miauto_universal_settings_fragment.xml |
| 3. Fragment 加载主 XML | 父类 BasePreferenceFragment | 主 XML 中包含「系统语言」「距离单位」「温度单位」等 Preference |
4. 调用 getPreferenceKeyResIdsToRemove() | UniversalSettingsFragment.java:69-80 | !isOverseasRegion() = !false … 注意 isOverseasRegion() = !isCnRegion(),isCnRegion() 对 "cn" 返回 true,所以 isOverseasRegion() = false,!isOverseasRegion() = true → 进入分支 |
| 5. 日志输出 | MLog.tag(TAG).i(...) | "Don't show system language preference, because not global region" |
| 6. 父类按 ids 移除 Preference | BasePreferenceFragment | 「系统语言」「距离单位」「温度单位」三项 Preference 被运行时移除 |
| 7. 最终 UI | — | 国内用户看不到上述三个入口 |
3.3 验证方法(adb)
# 1. 查 region(最关键的一步)
adb shell getprop ro.micar.build.region
# 国内典型输出: cn
# 海外全球典型输出: global
# 海外欧洲典型输出: eu
# 海外 ECE 典型输出: ece
# 2. 抓日志确认走了移除分支
adb logcat -s UniversalSettingsFragment | grep "Don't show system language"
# 国内版出现这条日志 = 入口被裁剪的铁证4. Flavor vs Region:两个正交维度(重要)
很多人会把 Flavor(dcd / xcd / global)和 Region(cn / global / eu / ece)搞混,认为是同一个东西。不是。
4.1 概念对照
| 维度 | Flavor | Region |
|---|---|---|
| 决定时机 | 编译期(Gradle product flavor) | 运行期(读 SystemProperty) |
| 来源 | app/build.gradle:13-25 的 productFlavors { dcd / xcd / global } | SystemProperty ro.micar.build.region(烧录镜像时由构建系统写入) |
| 维度语义 | 编译产物变体(资源目录前缀 dcddif / xcddif / region/global、依赖差异) | 车辆销售地区(同一 APK 跑在不同地区车上行为不同) |
| 变不变 | 一旦编译完 APK,flavor 就定了 | 同一 APK 在不同 region 的车上跑会呈现不同行为 |
| 取值数 | 3 个 | 4 个有效值(cn / global / eu / ece)+ 空 |
4.2 组合矩阵
| Flavor \ Region | cn | global | eu | ece |
|---|---|---|---|---|
| dcd(国内平台) | 国内裁剪(最常见的国内车型) | 海外全显(罕见,dcd 通常配 cn) | 海外全显 | ECE 精简 XML |
| xcd(小米平台) | XCD cn(题面情况):国内裁剪,看不到语言入口 | 海外全显 | 海外全显 | ECE 精简 XML |
| global(海外专版) | 国内裁剪(罕见,global flavor 一般不配 cn) | 海外全显(最常见的海外车型) | 海外全显 | ECE 精简 XML |
XCD cn 看不到语言入口的根因不在 Flavor——即使你把 XCD cn 换成
dcd cn或global cn,只要ro.micar.build.region是cn,语言入口一样被裁剪。Flavor 决定的是「APK 编译时打包了哪些资源和代码」,Region 决定的是「同一个 APK 在不同地区车上展示哪些功能」。
4.3 历史背景(猜测,待核实)
DeviceUtil那一套(isCnRegion / isOverseasRegion / isGlobalRegion / isEURegion)看起来是先有的,逻辑用!isCnRegion()当”海外”的快捷判定;MiCarSettings.isEce()看起来是后来加的,对应 ECE 法规地区的精简需求,单独挑出"ece"值用更激进的物理裁剪(换 XML)实现;- 两套逻辑读同一个 prop,但分两步判定(先判 ECE 换 XML,再在非 ECE 路径里判 cn 移除 key),不会冲突。
5. 在源码里改这类裁剪,需要注意什么
如果你接到一个需求:「国内版也要显示系统语言入口」——直接改 UniversalSettingsFragment.getPreferenceKeyResIdsToRemove(),把 R.string.pk_settings_system_universal_language 从 ids.add 列表里去掉即可。但要注意:
- 国内版真的需要语言切换吗? 历史上国内版隐藏语言入口可能是有意为之(国内默认中文,且无多语言资源运营)。改之前先和产品确认。
- SystemLanguagePreferenceController 是否能正确处理国内场景? 路径
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguagePreferenceController.java(待核实)。 - 多语言资源是否齐全? 国内版 APK 可能没打包其他语言的字符串资源,需要确认
app/src/region/cn/res/或values-xx/资源覆盖情况。 - 不要改 Flavor 维度去解决 Region 问题——很多人第一反应是「我把 flavor 改成 global 是不是就行」,但 flavor 改了之后整套资源目录都变了(看
app/build.gradle:68-86的sourceSets),影响面远超语言入口,会引入大量副作用。
6. 总结:一句话回答
XCD cn 看不到「系统语言」入口,是因为车机的
ro.micar.build.region属性值为cn,触发DeviceUtil.isCnRegion()=true→isOverseasRegion()=false,UniversalSettingsFragment.getPreferenceKeyResIdsToRemove()在国内分支中把pk_settings_system_universal_language加入移除列表,运行时被父类裁剪。与 Flavor(dcd/xcd/global)无关,与编译期无关,纯运行期行为。
相关文件清单
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/UniversalSettingsFragment.java:59-80(核心裁剪逻辑)base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/DeviceUtil.java:29,104-133(region 判定)base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/views/MiCarSettings.java:179-180,407-411(ECE 判定)settingsPage/settingsSystem/src/main/res/xml/miauto_universal_settings_fragment.xml(主版本 XML)settingsPage/settingsSystem/src/main/res/xml/miauto_universal_settings_fragment_ece.xml(ECE 精简 XML)settingsPage/settingsSystem/src/main/res/values/strings.xml:320-346(标题文案)app/build.gradle:11-25, 68-86(Flavor 与 sourceSets 定义)