01 - 系统语言切换全链路时序
知识库第 1 篇。本篇只回答一个问题:用户从「系统 → 通用 → 系统语言」点一下,到底发生了什么,到 locale 真正落盘为止。
一句话结论
入口 Controller 只是「拉起弹窗 + 显示当前语言」,真正的语言列表、选中态、落盘全部集中在 SystemLanguageDialogActivity:列表写死 10 条,单选互斥,点确定时通过 framework 的 LocalePicker.updateLocale() 把所选 locale 写回 ActivityManagerService 的系统配置,UI 的「刷新」完全依赖 onResume() 重新拉一次列表。
涉及角色
| 角色 | 文件 | 职责 |
|---|---|---|
SystemLanguagePreferenceController | settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguagePreferenceController.kt | 入口条目,显示当前语言、点击拉起弹窗 |
SystemLanguageDialogActivity | settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageDialogActivity.kt | 语言弹窗 Activity,承载列表 + 确定按钮 |
BaseConflictDialogActivity | base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseConflictDialogActivity.kt | 弹窗基类:冲替管理、Toolbar、确定/取消按钮容器 |
SystemLanguageAdapter | settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageAdapter.kt | 列表 Adapter,单选互斥 |
SystemLanguageConfig | settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageConfig.kt | 写死 10 种语言,标记选中 |
LocaleInfoWrapper | settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/LocaleInfoWrapper.kt | LocaleStore.LocaleInfo 的薄包装,暴露 id/name/summary/isSelected |
LocaleUtils | base/settingsBaseLib/src/main/java/com/android/micar/settings/utils/LocaleUtils.kt | 读当前系统 locale |
LocalePicker | com.android.internal.app.LocalePicker(framework) | 调用 updateLocale() 把 locale 写入系统配置 |
ActivityManagerService / system_locales | framework | locale 真正落盘的地方 |
全链路 Flowchart
flowchart TD A[用户进入 系统→通用] --> B[SystemLanguagePreferenceController.onCreateInternal] B --> C[updateRightText\n用 LocaleUtils.getConfiguredLocale +\nLocaleStore.getLocaleInfo.fullNameNative\n显示当前语言] C --> D[用户点击条目\nhandlePreferenceClicked] D --> E[startActivity SystemLanguageDialogActivity] E --> F[DialogActivityConflictManager.closeOtherDialog\n冲替: 关掉同类弹窗] F --> G[BaseConflictDialogActivity.onCreate\n初始化 Toolbar 与按钮容器] G --> H[SystemLanguageDialogActivity.onCreate\nsetContentView + initToolbar + initView] H --> I[initToolbar:\ntitle = system_universal_language_title\nnegativeButton.visibility = GONE\npositiveButton.text = system_universal_language_switch_confirm\npositiveButton.onClick -> 防双击 + 落盘] I --> J[onResume -> initData] J --> K[SystemLanguageConfig.getLanguageListWithSelected\n10 条语言 + 标记当前选中] K --> L[mAdapter.setData -> notifyDataSetChanged] L --> M[用户点击列表项] M --> M1{item.isSelected?} M1 -- 是 --> M[点击被吞 不变更] M1 -- 否 --> N[Adapter.selectItem:\nmCurrentSelected.isSelected = false\n本项.isSelected = true\nmCurrentSelected = 本项\nnotifyDataSetChanged] N --> O[用户点击 positiveButton 'OK'] O --> P{isDoubleClick\n300ms 内重复?} P -- 是 --> O[直接 return\n防连击] P -- 否 --> Q[mAdapter.getCurrentSelected] Q --> R{selectedLocale != null?} R -- 否 --> S[MLog.w 'selected locale is null'\nfinish] R -- 是 --> T[LocaleUtils.getConfiguredLocale\n读当前 system_locales 0] T --> U{selected == current?} U -- 是 --> S[短路 不调 updateLocale\n直接 finish] U -- 否 --> V[LocalePicker.updateLocale selectedLocale] V --> W[AMS 更新 system_locales\n应用层 Configuration 变更] W --> X[finish\n弹窗关闭] X --> Y[外层 Controller 再次 onResume] Y --> Z[updateRightText 刷新右侧文案\n显示新 locale.fullNameNative]
关键点:
- onResume 刷新:列表数据不缓存,每次
onResume()都重新getLanguageListWithSelected()+setData()。因此用户从外层回来时,当前语言标签会随系统 locale 同步。 - 防双击 isDoubleClick:确定按钮用全局函数
isDoubleClick(),DOUBLE_CLICK_INTERVAL = 300L毫秒,时钟为SystemClock.elapsedRealtime()(开机时间,免受系统时间回调影响)。 - 当前语言比较短路:点确定时如果
selected == current,不会调用updateLocale,直接finish(),避免无效 IPC。
全链路 Sequence Diagram
sequenceDiagram actor U as User participant C as SystemLanguagePreferenceController participant D as SystemLanguageDialogActivity participant B as BaseConflictDialogActivity participant A as SystemLanguageAdapter participant CF as SystemLanguageConfig participant LU as LocaleUtils participant LP as LocalePicker (framework) participant AMS as ActivityManagerService U->>C: 点击「系统语言」条目 Note over C: handlePreferenceClicked C->>C: getCurrentLanguage()<br/>LocaleUtils.getConfiguredLocale()<br/>LocaleStore.getLocaleInfo(...).fullNameNative C->>C: 条目右侧文案 = 当前语言 C->>D: startActivity(SystemLanguageDialogActivity) D->>B: super.onCreate (BaseConflictDialogActivity) B->>B: DialogActivityConflictManager<br/>.closeOtherDialog() 关掉同类冲替弹窗 B->>B: initCustomTitleView()<br/>显示 Toolbar D->>D: setContentView(micar_system_language_dialog) D->>D: initToolbar()<br/>title = "System language"<br/>negativeButton.visibility = GONE<br/>positiveButton.text = "OK" D->>D: initView()<br/>new SystemLanguageAdapter Note over D: onResume D->>CF: getLanguageListWithSelected() CF->>LU: getConfiguredLocale() LU->>AMS: ActivityManager.getService()<br/>.configuration.locales[0] AMS-->>LU: currentLocale LU-->>CF: currentLocale CF->>CF: 按固定顺序生成 10 条 LocaleInfoWrapper<br/>zh-Hans-CN / en-US / en-GB / 其他 7 条 A-Z<br/>命中 currentLocale 的那条 isSelected=true CF-->>D: List<LocaleInfoWrapper> D->>A: setData(list) A->>A: 找到 isSelected=true 的项<br/>记为 mCurrentSelected<br/>notifyDataSetChanged A-->>U: 渲染列表(10 行) U->>A: 点击某个未选中项 A->>A: setSingleClick -> isDoubleClick 守门<br/>item.isSelected==false 才进入 A->>A: selectItem(pos)<br/>mCurrentSelected.isSelected=false<br/>mList[pos].isSelected=true<br/>mCurrentSelected=mList[pos]<br/>notifyDataSetChanged A-->>U: 选中标记(对勾)移到新行 U->>D: 点击 positiveButton "OK" D->>D: isDoubleClick() 守门 (300ms) alt selectedLocale == null D->>D: MLog.w "selected locale is null" else selectedLocale != null D->>LU: getConfiguredLocale() LU->>AMS: 读 system_locales[0] AMS-->>LU: currentLocale alt selected == current Note over D: 短路, 不调 updateLocale else selected != current D->>LP: LocalePicker.updateLocale(selectedLocale) LP->>AMS: 更新 system_locales<br/>广播 ACTION_LOCALE_CHANGED Note over AMS: locale 真正落盘 end end D->>D: finish() Note over C: 外层 onResume C->>LU: getConfiguredLocale() LU-->>C: 新 currentLocale C->>C: updateRightText 刷新为<br/>新语言 fullNameNative
列表项选中态 State Diagram
stateDiagram-v2 [*] --> Unselected: 默认创建 Unselected --> Selected: 用户点击n(selectItem pos)nmCurrentSelected.isSelected=falsen本项.isSelected=truenmCurrentSelected=本项 Selected --> Unselected: 另一项被点击n(mCurrentSelected 被改成新项) Selected --> Selected: 用户再次点击本项nAdapter 守门: item.isSelected==truen点击被吞, 无变化 Selected --> Selected: onResume 重新 setDatan若仍是 currentLocalen保持选中 Unselected --> Selected: onResume 重新 setDatan若变成 currentLocalen被标记 isSelected=true
注意:Adapter 的点击守门(SystemLanguageAdapter.kt:59)只对「未选中项」放行,点击已选中项不会触发 selectItem,避免无效的 notifyDataSetChanged。
关键代码切片
1. 入口点击:拉起弹窗
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguagePreferenceController.kt:46
override fun handlePreferenceClicked(preference: BasicPreference): Boolean {
val intent = Intent(context, SystemLanguageDialogActivity::class.java)
context.startActivity(intent)
return true
}2. 入口显示当前语言
SystemLanguagePreferenceController.kt:52
private fun getCurrentLanguage(): String? {
val locale = LocaleUtils.getConfiguredLocale()
return LocaleStore.getLocaleInfo(locale)?.fullNameNative
}用
fullNameNative(语言在自身体系下的写法,例如中文环境下仍显示 “English (United States)”),不随 UI 语言变。
3. 读系统 locale
base/settingsBaseLib/src/main/java/com/android/micar/settings/utils/LocaleUtils.kt:15
fun getConfiguredLocale(): Locale {
return try {
val configLocale: Locale =
ActivityManager.getService().configuration.locales[0]
configLocale ?: Locale.getDefault()
} catch (e: Exception) {
MLog.tag(TAG).e("...")
Locale.getDefault()
}
}直接走
ActivityManager.getService()IPC 拿系统层 Configuration,不是本进程Resources.configuration,保证读到的是刚落盘的全局 locale。
4. 列表生成(写死 10 条,顺序固定)
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageConfig.kt:18 及 :62
fun getLanguageList(): List<LocaleInfoWrapper> {
val localeList = mutableListOf<LocaleInfoWrapper>()
// 1. 简体中文(中国)—— 用 "zh-Hans-CN" tag 显式构造
LocaleStore.getLocaleInfo(Locale.forLanguageTag("zh-Hans-CN"))?.let { localeList.add(LocaleInfoWrapper(it)) }
// 2. 美式英语
LocaleStore.getLocaleInfo(Locale.US)?.let { localeList.add(LocaleInfoWrapper(it)) }
// 3. 英式英语
LocaleStore.getLocaleInfo(Locale.UK)?.let { localeList.add(LocaleInfoWrapper(it)) }
// 4~10. 其他 7 种按原生名 A-Z 排序:de/es/fr/it/nb/nl/sv
val otherLangs = listOf("de", "es", "fr", "it", "nb", "nl", "sv")
// ... Collator.getInstance() 排序后追加
return localeList
}
fun getLanguageListWithSelected(): List<LocaleInfoWrapper> {
val localeList = getLanguageList()
val currentLocale = LocaleUtils.getConfiguredLocale()
for (localeWrapper in localeList) {
if (Objects.equals(localeWrapper.mLocaleInfo.locale, currentLocale)) {
localeWrapper.isSelected = true
return localeList
}
}
// 兜底:currentLocale 是 zh-CN 但未精确命中时,选 zh 那条
if (currentLocale.language == "zh" && currentLocale.country == "CN") {
localeList.firstOrNull { it.mLocaleInfo.locale.language == "zh" }?.isSelected = true
}
return localeList
}5. 单选互斥
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageAdapter.kt:96
private fun selectItem(position: Int) {
if (position !in mList.indices) {
return
}
mCurrentSelected?.isSelected = false
mList[position].isSelected = true
mCurrentSelected = mList[position]
notifyDataSetChanged()
}并在 onBindViewHolder 里做了「点击已选中项无效」的守门:
SystemLanguageAdapter.kt:56
holder.item.apply {
isSelected = item.isSelected
setSingleClick {
val pos = holder.adapterPosition
if (!item.isSelected && pos in mList.indices) {
selectItem(pos)
}
}
}6. 确定按钮:防双击 + 短路 + 落盘
settingsPage/settingsSystem/src/main/java/com/android/car/settings/universal/SystemLanguageDialogActivity.kt:42
positiveButton.setText(R.string.system_universal_language_switch_confirm)
positiveButton.setOnClickListener(View.OnClickListener {
if (isDoubleClick()) {
return@OnClickListener
}
val selectedLocale = mAdapter.getCurrentSelected()
if (selectedLocale != null) {
val currentLocale = LocaleUtils.getConfiguredLocale()
if (!Objects.equals(selectedLocale.mLocaleInfo.locale, currentLocale)) {
MLog.tag(TAG).i("updateLocale locale to ${selectedLocale.mLocaleInfo.locale}")
LocalePicker.updateLocale(selectedLocale.mLocaleInfo.locale)
}
} else {
MLog.tag(TAG).w("selected locale is null")
}
finish()
})7. onResume 重新拉列表
SystemLanguageDialogActivity.kt:34
override fun onResume() {
super.onResume()
initData()
}
private fun initData() {
mFunctionData = SystemLanguageConfig.getLanguageListWithSelected()
mAdapter.setData(mFunctionData)
}8. 防双击实现
base/settingsBaseLib/src/main/java/com/android/car/settings/common/SettingsExt.kt:137
const val DOUBLE_CLICK_INTERVAL = 300L
fun isDoubleClick(): Boolean {
val currentTime = SystemClock.elapsedRealtime()
clickedTime = if (clickedTime == -1L) {
currentTime
} else if ((currentTime - clickedTime) < DOUBLE_CLICK_INTERVAL) {
return true
} else {
currentTime
}
return false
}注意
clickedTime是模块级var(SettingsExt.kt:121),全局共享:弹窗的「确定」按钮和外层条目的setSingleClick用的是同一个时间戳,因此连续操作(点列表 → 立刻点确定)也可能命中 300ms 闸门。待核实:是否在实测中出现误吞。
9. 冲替基类:关掉同类弹窗
base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseConflictDialogActivity.kt:27
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (isConflictDialog()) {
DialogActivityConflictManager.addCallback(this)
DialogActivityConflictManager.closeOtherDialog(this::class.java.simpleName)
}
// ...
}SystemLanguageDialogActivity.kt:74 重写 isConflictDialog() = true,因此只要本弹窗起来,其它已存在的 SystemLanguageDialogActivity 实例会被 close。这是单例式的「冲替」语义。
10. positiveButton / negativeButton 的来源
SystemLanguageDialogActivity 里直接写 positiveButton / negativeButton / title,这些不是 BaseConflictDialogActivity 自己的字段,而是其父类 micarx.appcompat.app.BaseDialogActivity(外部库 MiCarSupport/appcompat)通过 getter 暴露的合成属性:
/home/zbc/car/MiCarSupport/appcompat/src/main/java/micarx/appcompat/app/BaseDialogActivity.java:392
@Override
public Button getPositiveButton() {
if (mDialogFrame != null) {
return mDialogFrame.getPositiveButton();
}
return null;
}Kotlin 端用 positiveButton.setText(...)、negativeButton.visibility = View.GONE 直接访问。
用户可见文案(直抄 strings.xml)
| 用途 | string name | 英文(values/strings.xml) | 中文(values-zh-rCN/strings.xml) |
|---|---|---|---|
| 弹窗页面标题(Toolbar title) | system_universal_language_title | System language | 系统语言 |
| 确定按钮文字(positiveButton) | system_universal_language_switch_confirm | OK | 确定 |
来源:
settingsPage/settingsSystem/src/main/res/values/strings.xml:325<string name="system_universal_language_title">System language</string>settingsPage/settingsSystem/src/main/res/values/strings.xml:326<string name="system_universal_language_switch_confirm">OK</string>settingsPage/settingsSystem/src/main/res/values-zh-rCN/strings.xml:295<string name="system_universal_language_title">系统语言</string>settingsPage/settingsSystem/src/main/res/values-zh-rCN/strings.xml:296<string name="system_universal_language_switch_confirm">确定</string>
取消按钮
negativeButton在initToolbar()里被View.GONE(SystemLanguageDialogActivity.kt:41),用户看不到。
设计要点总结
- 职责单一:入口 Controller 不持有列表逻辑,列表/选中/落盘全部在
SystemLanguageDialogActivity内部完成,方便后续多 Flavor 替换弹窗而不动外层。 - 写死列表:10 种语言不依赖系统
LocaleConfig,避免海外不同 ROM build 出现列表漂移;顺序也写死,前后端测试有稳定预期。 - 状态无持久化:选中态只在内存 (
mCurrentSelected),不做缓存。每次onResume()重新计算,规避「系统 locale 在弹窗外面被改过」导致 UI 残影。 - 三层守门:列表项点击有
isDoubleClick+isSelected双闸;确定按钮有isDoubleClick+selected == current短路;落盘前再getConfiguredLocale()读一次系统当前值,最大限度减少无谓的 IPC 和广播。 - 落盘靠 framework:本应用不直接写
Settings.System或AssetManager,而是LocalePicker.updateLocale(),由 framework 统一广播ACTION_LOCALE_CHANGED,全机一致性由 framework 保证。