04 · adb 实操与排查手册(fermi 实战复盘 + 入口盘点)

适用范围:MiCarSettings 项目内所有”切换系统语言”相关代码路径;机型以 fermi (flavor=xcd) 实战为基线,对外推 modena / suiren / kunlun 等同理。

前置阅读:本系列 01~03 篇(语言切换总体设计、LocalePicker 链路、LocaleChangedReceiver 通知语音)。


0. TL;DR(懒人速查)

想做的事一行命令
查当前 region(决定国内/海外)adb shell getprop ro.micar.build.region
查当前语言(系统属性)adb shell getprop persist.sys.locale
查当前语言(SettingsProvider,权威adb shell settings get system system_locales
查当前 Configuration 实际生效值adb shell dumpsys activity | grep -oE '\[[a-z]{2}_[A-Z]{2,3}\]' | head -1
切到英文(推荐路径adb shell settings put system system_locales en-US + adb reboot
切回中文adb shell settings put system system_locales zh-CN + adb reboot

一句话总结:在 MiCarSettings 上切语言,权威源是 settings system system_locales,不是 persist.sys.locale。只改 prop 不生效,必须改 system_locales 后重启。


1. 排查决策树(用户说”看不到语言入口 / 想切语言”)

flowchart TD
    Q[用户反馈: 找不到语言入口<br/>或想切换语言] --> R0[第一步: 查 region]
    R0 --> R0cmd["adb shell getprop ro.micar.build.region"]
    R0cmd --> Rjudge{region == 空 / cn ?}

    Rjudge -- 是: 国内版 --> CN_PATH[国内版路径<br/>UniversalSettingsFragment 在 onCreate 时<br/>把 pk_settings_system_universal_language<br/>从 XML 移除 → 用户看不到系统语言入口]
    CN_PATH --> CN_BYPASS[没有官方 UI<br/>只能 adb 直接改 system_locales]
    CN_BYPASS --> SETSL[adb shell settings put system system_locales en-US]
    SETSL --> REBOOT1[adb reboot]
    REBOOT1 --> VERIFY[验证三连]

    Rjudge -- 否: 海外 global/eu --> GW_PATH[海外版路径<br/>设置 → 通用 → 系统语言 → SystemLanguageDialogActivity]
    GW_PATH --> GW_UI[用户点 UI 选语言<br/>走 LocalePicker.updateLocale]
    GW_UI --> VERIFY

    VERIFY --> V1[adb shell getprop persist.sys.locale]
    VERIFY --> V2[adb shell settings get system system_locales]
    VERIFY --> V3[adb shell dumpsys activity grep Configuration]
    V1 --> OK{三者一致?}
    V2 --> OK
    V3 --> OK
    OK -- 是 --> DONE[切换成功<br/>ACTION_LOCALE_CHANGED 广播已发<br/>语音搜索自动重解析]
    OK -- 否 --> FALLBACK[回到 SETSL: 改 system_locales<br/>再 reboot]

要点:

  • “看不到入口”≠“不能切”,国内版故意把入口移除(产品策略锁定中文),底层能力仍在。
  • region 的判定源是 ro.micar.build.region(见 base/settingsBaseLib/.../DeviceUtil.java:29,131-133),与 flavor(dcd/xcd/global)正交——xcd + cn 组合完全合法,本次 fermi 就是这一组合。

2. 命令清单(按用途分组)

2.1 查询类(只读,无 root 要求)

命令作用是否需 root备注
adb shell getprop ro.micar.build.region取出机型地区(cn / global / eu)国内判定源,空也算国内
adb shell getprop persist.sys.locale取出系统属性里的 locale(如 zh-CN非权威,仅供参考;改它不一定生效
adb shell settings get system system_locales取出 SettingsProvider 中的 locale 列表权威源,决定 Configuration
adb shell dumpsys activity | grep -oE '\[[a-z]{2}_[A-Z]{2,3}\]' | head -1看 ActivityManagerService 真实 Configuration真正在跑的 locale,最准
adb shell getprop ro.build.type看是 user / userdebug / eng决定后续能否 setprop
adb shell dumpsys package com.android.car.settings | grep -i 'granted=true'看 Settings 是否具备 CHANGE_CONFIGURATION 等权限排查代码层切换失败时有用

2.2 切换类(写操作)

命令作用是否需 root生效条件
adb shell settings put system system_locales en-US把权威 locale 改为 en-US必须 reboot,重启后 ActivityManager 读取并写入 Configuration
adb shell settings put system system_locales zh-CN改回简体中文同上
adb shell setprop persist.sys.locale en-US改系统属性里的 locale(user 版 setprop 受限)非权威:单独改这一项不会重置 Configuration,本次 fermi 第 1 次失败的根因
adb shell am broadcast -a android.intent.action.LOCALE_CHANGED手动补发语言切换广播仅触发已注册 Receiver 重解析,不真改 locale;排查 Receiver 用
adb shell pm grant com.android.car.settings android.permission.CHANGE_CONFIGURATION给 Settings 应用授予 CHANGE_CONFIGURATION(如要走代码路径却失败时)signature 权限,常规版型已具备

2.3 验证类(写完必须看)

# 三件套,缺一不可
adb shell getprop persist.sys.locale               # 期望: en-US
adb shell settings get system system_locales       # 期望: en-US
adb shell dumpsys activity | grep -oE '\[[a-z]{2}_[A-Z]{2,3}\]' | head -1   # 期望: [en_US]

三者一致才算切干净。三者不一致时以第三个(Configuration)为准。


3. fermi 实战复盘(flavor=xcd, region=cn, adbd 已有 root)

3.1 初始状态

ro.micar.build.region = cn          → 国内版,系统语言入口被隐藏
ro.build.type         = user        → 但 adbd 已是 root("adbd is already running as root")
persist.sys.locale    = zh-CN
system_locales        = zh-CN
Configuration         = [zh_CN]

3.2 完整命令序列与失败/成功时序

sequenceDiagram
    autonumber
    participant ENG as 工程师
    participant ADB as adb shell
    participant PROP as persist.sys.locale<br/>(SystemProperties)
    participant SET as system_locales<br/>(SettingsProvider)
    participant AMS as ActivityManager<br/>Configuration
    participant SETT as MiCarSettings UI

    Note over ENG,AMS: 初始: zh-CN / zh-CN / [zh_CN]

    Note right of ENG: 第 1 次: 只改 prop (失败案例)
    ENG->>ADB: setprop persist.sys.locale en-US
    ADB->>PROP: 写入 en-US ✅
    ENG->>ADB: reboot
    ADB->>AMS: 重启后,AMS 读 SET(system_locales) 构造 Configuration
    AMS->>SET: 读 system_locales = zh-CN (没被改)
    AMS-->>ENG: Configuration 仍是 [zh_CN] ❌
    SETT-->>ENG: UI 仍中文 ❌

    Note right of ENG: 第 2 次: 改权威源 system_locales (成功)
    ENG->>ADB: settings put system system_locales en-US
    ADB->>SET: 写入 en-US ✅
    ENG->>ADB: reboot
    ADB->>AMS: 重启后读 system_locales = en-US
    AMS->>AMS: Configuration=[en_US] ✅
    AMS-->>ENG: ACTION_LOCALE_CHANGED 广播
    ENG->>ADB: 三连验证
    ADB-->>ENG: persist=en-US / system_locales=en-US / [en_US] ✅
    SETT-->>ENG: UI 切英文 ✅
    Note over ENG,AMS: LocaleChangedReceiver 收到广播<br/>→ VoiceSearchManager.onLocaleChanged()<br/>→ 语音搜索重解析

3.3 第 1 次失败的根因(重要)

persist.sys.locale 是一个 历史 fallback,在 AOSP 早期是 locale 的存放点;但在 Android 7.0+ 以后 system_locales(SettingsProvider 中的多 locale 列表)才是权威:

  • ActivityManagerService 启动时优先读 Settings.System.SYSTEM_LOCALES,再回退到 persist.sys.locale
  • 一旦 system_locales 不为空,它就完全说了算;persist.sys.locale 仅作兼容性提示,改它不会反向写回 system_locales
  • 所以单独 setprop 然后 reboot,AMS 读到的还是旧的 zh-CN,Configuration 不变。

正确做法是改权威源:adb shell settings put system system_locales en-US,然后重启让 AMS 重新构造 Configuration。

3.4 第 2 次成功的机制

settings put system system_locales en-US 直接写到 /data/system/users/0/settings_system.xml,重启后:

  1. ActivityManagerService 启动 → 调 LocaleManagerSettings.System.SYSTEM_LOCALES → 得到 en-US
  2. 构造 Configuration,置 configLocales = [en_US]userSetLocale = true
  3. Configuration 更新后系统广播 android.intent.action.LOCALE_CHANGED
  4. LocaleChangedReceiver(注册在 app/src/main/AndroidManifest.xml:1583-1589)收到广播,调 VoiceSearchManager.onLocaleChanged() 让语音索引重新解析。
  5. UI(Resources)按新 Configuration 加载 strings → 英文界面。

4. 项目内 4 个语言切换入口盘点

#入口文件位置触发场景国内可用调用链 / 实现要点存活状态
设置 → 通用 → 系统语言(主入口)settingsPage/settingsSystem/.../universal/SystemLanguagePreferenceController.kt:46-50 + SystemLanguageDialogActivity.kt:18-76用户点 Preference 项 → 拉起 DialogActivity → RecyclerView 列表 → 点确认(国内版被 UniversalSettingsFragment 在 onCreate 时移除 Preference,见下文)handlePreferenceClickedstartActivity(SystemLanguageDialogActivity)LocalePicker.updateLocale(locale) (com.android.internal.app.LocalePicker)在用(仅海外版)
开机引导选择语言(首次开机)settingsPage/settingsSystem/.../universal/ProvisionLanguageDialogActivity.kt:31-105首次开机或恢复出厂后由开机引导流程拉起;对外 action com.android.car.settings.provision.LOCALE_SETTINGSapp/src/main/AndroidManifest.xml:802-813是(国内外均注册,由 provision 流程决定何时拉起)onResume → initData → SystemLanguageConfig.getLanguageListWithSelected → 用户选中 → confirmUpdateLocale()LocalePicker.updateLocale()(行 100)在用
显示设置老入口(Tab 切换)settingsPage/micarDisplaySettings/.../miauto/display/LanguagePickerPrefController.java:34-68显示设置页面 Tab 切换中英文(XML 中整段被注释,见下文)onSelectTabChangedLocalePicker.updateLocale(locale),调用的是 framework com.android.internal.app.LocalePicker(与同包 display/language/LocalePicker.java 同名但完全无关,后者无活跃调用方,见 §4.2)已废弃(XML 注释停用)
彩蛋:内核版本点 10 次settingsPage/settingsSystem/.../system/KernelVersionPreferenceController.java:80-94LanguageUtils.showTipToastutils/LanguageUtils.java:17-44系统 → 参数信息 → 连点”内核版本” 10 次 → 弹中英互切弹窗部分(代码层无 region 判断、sChangeLanguageEnabled = true 硬编码;但 micar_about_settings_fragment.xml:39hiddenFeatures="lemans_ece" 在 ECE 版隐藏内核版本条目 → ECE 点不到;国内 cn 版正常可见handlePreferenceClicked 计数到 0 → LanguageUtils.showTipToast → 弹框确认 → LocalePicker.updateLocale(反值)在用(隐藏彩蛋)

4.1 国内版为何看不到入口 ① 的代码位置

settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/UniversalSettingsFragment.java

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;
}

DeviceUtil.isOverseasRegion() 的判定(base/settingsBaseLib/.../DeviceUtil.java:122-133):

public static boolean isOverseasRegion() { return !isCnRegion(); }
public static boolean isCnRegion() {
    return TextUtils.isEmpty(BUILD_REGION) || BUILD_REGION_CN.equals(BUILD_REGION);
}
// BUILD_REGION = SystemProperties.get("ro.micar.build.region")  // 行 29
// BUILD_REGION_CN = "cn"                                       // 行 34

结论:ro.micar.build.region 为空或 cn 时,入口 ① 在 Fragment 初始化阶段被运行时移除,用户无法看到也无法点击——本次 fermi (region=cn) 走的就是这条路径,所以才必须用 adb 绕过。

4.2 入口 ③ 的 XML 注释证据

settingsPage/micarDisplaySettings/src/main/res/xml/miauto_display_atmosphere_settings_fragment.xml

            android:key="@string/pk_settings_display_locale_picker_entry"
            android:title="@string/settings_universal_title_language_picker"
            settings:controller="com.android.car.settings.miauto.display.LanguagePickerPrefController" />&ndash;&gt;
    </com.android.car.settings.miauto.preferences.MiCarPreferenceCategory>-->

整段 PreferenceCategory 都被 XML 注释包裹(&ndash;&gt;--> 的 HTML 转义形式),所以 LanguagePickerPrefController 不会被实例化、display/language/LocalePicker.java 也不会被调用。保留代码仅为历史追溯,不应在新特性里引用

4.3 入口 ④ 彩蛋触发后的 UI 文案(直抄 strings.xml)

settingsPage/settingsSystem/src/main/res/values/strings.xml

<string name="micar_change_locale_dialog_title">"Change language"</string>           <!-- 行 204 -->
<string name="micar_change_locale_dialog_centent">"系统语言将切换为英文。System language will change to English."</string>  <!-- 行 205 -->
<string name="micar_change_locale_dialog_btn_cancel">"Cancel"</string>               <!-- 行 206 -->
<string name="micar_change_locale_dialog_btn_confirm">"OK"</string>                  <!-- 行 207 -->
<string name="micar_setting_changed_language">You\'ve set your system language to English now</string>  <!-- 行 209 -->

中文译文(values-zh-rCN/strings.xml:175-181):标题”切换语言”,按钮”取消/确定”。

注意:这个彩蛋写死了只在中英文互切LanguageUtils.java:23-24),不是通用 locale 选择器。

4.4 入口 ② 开机引导的对外 action

app/src/main/AndroidManifest.xml:808-811

<intent-filter android:priority="100">
    <action android:name="com.android.car.settings.provision.LOCALE_SETTINGS" />
    <category android:name="android.intent.category.DEFAULT"/>
</intent-filter>

可用 adb 模拟 provision 拉起(用于调试,不会真正切换,要用户点确认):

adb shell am start -a com.android.car.settings.provision.LOCALE_SETTINGS

5. 常见坑

坑 ① 只改 persist.sys.locale 不生效

  • 现象:setprop 成功 + reboot,但 Configuration 不变、UI 还是中文。
  • 根因:system_locales 才是权威,单独改 prop 不反向同步。
  • 对策:必走 settings put system system_locales <locale> + reboot。

坑 ② user 版无 root 怎么办

user 版 adb root 通常被禁,三种可行替代:

  1. 刷 userdebugmibuild.sh -l <flavor>-userdebug 编译并烧机,得到 root shell。一劳永逸,调试推荐。
  2. 走代码路径:在有 CHANGE_CONFIGURATION 权限的前提下(系统签名应用默认有),在任意 PreferenceController 里调 com.android.internal.app.LocalePicker.updateLocale(Locale.US),参考入口 ④ 彩蛋的实现。临时调试可在 BaseActivity 或某个开发用页面加按钮触发。
  3. 借开机引导入口adb shell am start -a com.android.car.settings.provision.LOCALE_SETTINGS 拉起 ProvisionLanguageDialogActivity,让用户在 UI 上选——前提是该 Activity 在当前机型注册(app/src/main/AndroidManifest.xml:802 已默认注册)。

坑 ③ 国内版”锁定中文”的产品原因

  • 国内车型投放市场单一,多语言 UI 会带来翻译不全 / 售后误解 / 法务标签不一致等成本。
  • 工程实现:UniversalSettingsFragment 在国内 region 把”系统语言/距离单位/温度单位”三项 Preference 一并移除(UniversalSettingsFragment.java 行 72-78),不止语言。
  • 底层能力未禁用:adb 改 system_locales、彩蛋、开机引导都仍可切。这是”产品层隐藏”而非”系统层禁用”。

坑 ④ 改了之后个别模块仍显示旧语言

  • 通常是 ACTION_LOCALE_CHANGED 广播没传到。先 adb logcat | grep LocaleChangedReceiver 看是否收到。
  • 手动补发:adb shell am broadcast -a android.intent.action.LOCALE_CHANGED
  • 若某第三方进程缓存的 Configuration 没刷新,杀掉重启即可:adb shell am force-stop <pkg>

坑 ⑤ 切回中文后某些海外专属字符串报错

  • 海外版(global/eu)有专属资源目录(如 values-devalues-fr),切回 zh-CN 后这些目录不会被加载,正常现象。但若代码里硬引用了 R.string.xxx 而该 string 仅在 region/global flavor 下编译,会编译失败/运行崩溃——属 flavor 与 region 组合的兼容性问题,与 locale 切换本身无关。

6. 切回中文(反向操作)

# 主路径:改权威源 + 重启
adb shell settings put system system_locales zh-CN
adb reboot
 
# 可选:同步刷新 prop(便于 logcat / 第三方读取一致性)
adb shell setprop persist.sys.locale zh-CN    # user 版需 root
 
# 验证三连(应全部为 zh-CN / [zh_CN])
adb shell getprop persist.sys.locale
adb shell settings get system system_locales
adb shell dumpsys activity | grep -oE '\[[a-z]{2}_[A-Z]{2,3}\]' | head -1

如果只是临时验证又不想重启,可用代码路径(需应用具备 CHANGE_CONFIGURATION 权限):

// 等价于入口 ④ 彩蛋的核心调用
com.android.internal.app.LocalePicker.updateLocale(java.util.Locale.SIMPLIFIED_CHINESE)

7. 一页纸总结

flowchart LR
    subgraph Authority["权威源"]
        SL["settings system<br/>system_locales"]
    end
    subgraph Shadow["影子源(非权威)"]
        PL[persist.sys.locale]
    end
    subgraph Effect["运行时生效"]
        CF[Configuration]
        UI[Resources / UI]
        BR[ACTION_LOCALE_CHANGED<br/>广播]
        VSM[VoiceSearchManager<br/>重解析]
    end

    SL -- "reboot 后 AMS 读取" --> CF
    PL -. "fallback only<br/>单独改无效" .-> CF
    CF --> UI
    CF --> BR
    BR --> VSM

记住三句话:

  1. system_locales,不要只改 prop。
  2. 改完必须 reboot(或走代码路径调 LocalePicker.updateLocale)。
  3. 三个查询命令都要看,以 dumpsys activity 的 Configuration 为准。