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,重启后:
ActivityManagerService启动 → 调LocaleManager读Settings.System.SYSTEM_LOCALES→ 得到en-US。- 构造
Configuration,置configLocales = [en_US]、userSetLocale = true。 Configuration更新后系统广播android.intent.action.LOCALE_CHANGED。LocaleChangedReceiver(注册在app/src/main/AndroidManifest.xml:1583-1589)收到广播,调VoiceSearchManager.onLocaleChanged()让语音索引重新解析。- UI(Resources)按新 Configuration 加载 strings → 英文界面。
4. 项目内 4 个语言切换入口盘点
| # | 入口 | 文件位置 | 触发场景 | 国内可用 | 调用链 / 实现要点 | 存活状态 |
|---|---|---|---|---|---|---|
| ① | 设置 → 通用 → 系统语言(主入口) | settingsPage/settingsSystem/.../universal/SystemLanguagePreferenceController.kt:46-50 + SystemLanguageDialogActivity.kt:18-76 | 用户点 Preference 项 → 拉起 DialogActivity → RecyclerView 列表 → 点确认 | 否(国内版被 UniversalSettingsFragment 在 onCreate 时移除 Preference,见下文) | handlePreferenceClicked → startActivity(SystemLanguageDialogActivity) → LocalePicker.updateLocale(locale) (com.android.internal.app.LocalePicker) | 在用(仅海外版) |
| ② | 开机引导选择语言(首次开机) | settingsPage/settingsSystem/.../universal/ProvisionLanguageDialogActivity.kt:31-105 | 首次开机或恢复出厂后由开机引导流程拉起;对外 action com.android.car.settings.provision.LOCALE_SETTINGS(app/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 中整段被注释,见下文) | onSelectTabChanged → LocalePicker.updateLocale(locale),调用的是 framework com.android.internal.app.LocalePicker(与同包 display/language/LocalePicker.java 同名但完全无关,后者无活跃调用方,见 §4.2) | 已废弃(XML 注释停用) |
| ④ | 彩蛋:内核版本点 10 次 | settingsPage/settingsSystem/.../system/KernelVersionPreferenceController.java:80-94 调 LanguageUtils.showTipToast(utils/LanguageUtils.java:17-44) | 系统 → 参数信息 → 连点”内核版本” 10 次 → 弹中英互切弹窗 | 部分(代码层无 region 判断、sChangeLanguageEnabled = true 硬编码;但 micar_about_settings_fragment.xml:39 的 hiddenFeatures="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" />–>
</com.android.car.settings.miauto.preferences.MiCarPreferenceCategory>-->整段 PreferenceCategory 都被 XML 注释包裹(–> 是 --> 的 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_SETTINGS5. 常见坑
坑 ① 只改 persist.sys.locale 不生效
- 现象:
setprop成功 + reboot,但 Configuration 不变、UI 还是中文。 - 根因:
system_locales才是权威,单独改 prop 不反向同步。 - 对策:必走
settings put system system_locales <locale>+ reboot。
坑 ② user 版无 root 怎么办
user 版 adb root 通常被禁,三种可行替代:
- 刷 userdebug:
mibuild.sh -l <flavor>-userdebug编译并烧机,得到 root shell。一劳永逸,调试推荐。 - 走代码路径:在有 CHANGE_CONFIGURATION 权限的前提下(系统签名应用默认有),在任意 PreferenceController 里调
com.android.internal.app.LocalePicker.updateLocale(Locale.US),参考入口 ④ 彩蛋的实现。临时调试可在BaseActivity或某个开发用页面加按钮触发。 - 借开机引导入口:
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-de、values-fr),切回zh-CN后这些目录不会被加载,正常现象。但若代码里硬引用了R.string.xxx而该 string 仅在region/globalflavor 下编译,会编译失败/运行崩溃——属 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
记住三句话:
- 改
system_locales,不要只改 prop。 - 改完必须 reboot(或走代码路径调
LocalePicker.updateLocale)。 - 三个查询命令都要看,以
dumpsys activity的 Configuration 为准。