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 Keyro.micar.build.regionbase/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/DeviceUtil.java:29
BUILD_REGIONSystemProperties.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 keybase/settingsBaseLib/src/main/java/com/android/car/settings/miauto/common/views/MiCarSettings.java:179
ECE_REGION"ece"MiCarSettings.java:180

关键发现DeviceUtil.BUILD_REGIONMiCarSettings.ROM_BUILD_REGION 读取的是同一个 SystemProperty ro.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.xmlECE 版 miauto_universal_settings_fragment_ece.xmlstring 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 判定决策树

DeviceUtilro.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 移除 PreferenceBasePreferenceFragment「系统语言」「距离单位」「温度单位」三项 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 概念对照

维度FlavorRegion
决定时机编译期(Gradle product flavor)运行期(读 SystemProperty)
来源app/build.gradle:13-25productFlavors { 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 \ Regioncnglobaleuece
dcd(国内平台)国内裁剪(最常见的国内车型)海外全显(罕见,dcd 通常配 cn)海外全显ECE 精简 XML
xcd(小米平台)XCD cn(题面情况):国内裁剪,看不到语言入口海外全显海外全显ECE 精简 XML
global(海外专版)国内裁剪(罕见,global flavor 一般不配 cn)海外全显(最常见的海外车型)海外全显ECE 精简 XML

XCD cn 看不到语言入口的根因不在 Flavor——即使你把 XCD cn 换成 dcd cnglobal cn只要 ro.micar.build.regioncn,语言入口一样被裁剪。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_languageids.add 列表里去掉即可。但要注意:

  1. 国内版真的需要语言切换吗? 历史上国内版隐藏语言入口可能是有意为之(国内默认中文,且无多语言资源运营)。改之前先和产品确认。
  2. SystemLanguagePreferenceController 是否能正确处理国内场景? 路径 settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguagePreferenceController.java(待核实)。
  3. 多语言资源是否齐全? 国内版 APK 可能没打包其他语言的字符串资源,需要确认 app/src/region/cn/res/values-xx/ 资源覆盖情况。
  4. 不要改 Flavor 维度去解决 Region 问题——很多人第一反应是「我把 flavor 改成 global 是不是就行」,但 flavor 改了之后整套资源目录都变了(看 app/build.gradle:68-86sourceSets),影响面远超语言入口,会引入大量副作用。

6. 总结:一句话回答

XCD cn 看不到「系统语言」入口,是因为车机的 ro.micar.build.region 属性值为 cn,触发 DeviceUtil.isCnRegion()=trueisOverseasRegion()=falseUniversalSettingsFragment.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 定义)