02 · 整体架构与流程图
本篇用图把模块讲透。建议配合 03/04 篇的 file:line 一起看。
1. 分层架构总览
flowchart TB subgraph L1["① UI 层(MiCarSettings · settingsBaseUi)"] XML["connection_settings_fragment.xml:121<br/>NewTabPreference"] VB["NewTabViewBinder / CarUiTabLayout<br/>三段 Tab 渲染与点击分发"] end subgraph L2["② 控制器层(MiCarSettings · micarConnectionSettings/main)"] CTRL["TeleCommunicationSelectController<br/>Tab 数据/选中态/ECALL 禁用/持久化"] end subgraph L3["③ 平台桥接层(MiCarSettings · 按 flavor 三选一)"] UTIL["TelephonyUtil(object,反射封装)<br/>xcddif = f3dif(全功能)<br/>dcddif(空桩,DCD 无此功能)"] end subgraph L4["④ Framework 平台 API(pangu/yu7)"] EX["TelephonyManagerEx(抽象)<br/>:117 setUserDefaultChannel<br/>:118 getUserDefaultChannel"] IMPL["TelephonyManagerImpl:736-758<br/>→ IAutoTelephony(binder)"] end subgraph L5["⑤ Radio 进程 · 平台策略(pangu/yu7)"] APM["AutoPhoneInterfaceManager<br/>(binder 入口)"] ROUTE["MultiInternetRouteController<br/>受理层:校验+记偏好+转发,恒 true"] SMART["SmartDataChannelController<br/>执行层:rat 校验+真正切通道"] end MODEM[("modem / UICC<br/>卡1=电信 卡2=移动")] XML --> VB --> CTRL --> UTIL --> EX --> IMPL --> APM --> ROUTE --> SMART --> MODEM
各层职责一句话:
| 层 | 职责 | 出问题时的典型症状 |
|---|---|---|
| UI 层 | 渲染 Tab、分发点击、高亮态 | 点击无反应、高亮错乱 |
| 控制器层 | 业务逻辑、偏好持久化、ECALL 禁用 | 选中态丢失/错乱、summary 不更新 |
| 桥接层 | 反射调平台,屏蔽 flavor 差异 | NoSuchMethod 日志、恒返回 false/-1 |
| 平台 API | binder 通道 | binder 超时/冻结 |
| 受理层 | 记偏好、立即返回 | 「返回 true 但网没切」 |
| 执行层 | 真正切数据通道,带断网保护 | ignore due to rat=0 ! |
2. 类关系图(App 侧)
classDiagram class NewTabPreference { +setData(Array~Tab~, Button?) +setTabCallback(ITabCallback) -mTabClickCallBack 点击路由 } class BaseTabPreferenceController { <<abstract>> +buildTabData()* Array~Tab~ +onSelectTabChanged(tab) +onBuildMiCarViewDescription(tab) } class TeleCommunicationSelectController { -mSelectionTabData 三个 Tab +onResumeInternal() 校准选中态 +onTelephonySIMCardSelectChanged() 下发平台 -updateEnableState() ECALL 禁用 } class TelephonyUtil { <<object>> +setDefaultChannel(int) Boolean +getDefaultChannel() Int } class CarUiTabLayout { +Tab~T~(title).setData(T) +Button~T~ } NewTabPreference <|-- BaseTabPreferenceController : 持有 preference 泛型 BaseTabPreferenceController <|-- TeleCommunicationSelectController TeleCommunicationSelectController ..> CarUiTabLayout : 构造 Tab 数据 TeleCommunicationSelectController ..> TelephonyUtil : 读写通道 NewTabPreference ..> CarUiTabLayout : NewTabViewBinder 渲染
点击路由规则(NewTabPreference.kt:60-72,极易踩坑):
flowchart TD Click[用户点击某 Tab] --> Avail{tab.isAvailable?} Avail -- "否(ECALL 置灰)" --> Allow{allowClickWhenDisabled?} Allow -- 是 --> Disabled["onDisabledTabClicked()<br/>弹「切换失败」框"] Allow -- 否 --> Nothing["什么都不做"] Avail -- 是 --> Sel{tab.mIsSelected?} Sel -- "已是选中项" --> Resel["onSelectedTabClicked()<br/>本模块空实现"] Sel -- 否 --> Change["onSelectTabChanged()<br/>→ 走切换流程"]
3. 核心流程一:点击切换(全链路时序)
sequenceDiagram autonumber participant U as 用户 participant V as NewTabPreference participant C as TeleCommunicationSelectController participant T as TelephonyUtil(反射) participant P as 平台 TelephonyManagerEx participant R as MultiInternetRouteController participant S as SmartDataChannelController U->>V: 点击「中国移动」 V->>C: onSelectTabChanged(tab=CMCC) C->>C: mIsSelected 乐观置真(:63-67 父类)<br/>refreshSubTitle → summary 换文案(:150) C->>+T: ThreadPoolUtils.ASYNC 异步调<br/>setDefaultChannel(1) (:164-183) T->>+P: 反射 setUserDefaultChannel(1) P->>+R: IAutoTelephony binder R->>R: 校验合法/非重复(:392-399)<br/>写 Settings.Global[HMI_DATA_SIM_APN]=1 R->>+S: setApnDefaultChannel(MULTI_INTERNET_2, 1, REASON_HMI_SET) S->>S: 校验 phoneId + rat(必须 LTE/NR)<br/>不满足 → localLog "ignore due to rat=0 !"<br/>返回 false(:1709-1725) S-->>-R: 返回 false(被丢弃!) R-->>-P: **无条件 return true**(受理语义) P-->>-T: true T-->>-C: result = true C->>C: 写 Settings.Global<br/>[VEHICLE_MOBILE_NETWORK_SELECTION]=1 (:185) Note over C: 若 result=false:mUIHandler.post 回滚 UI 到旧选中(:177-179)
这张图的两个要害:
- 第 13 步的 false 被第 14 步吃掉——执行层拒绝(比如目标卡无服务)不会传导回 App, App 看到的永远是「受理成功」。这是 N801PROB-138847 的契约层根因(详见 04/05 篇)。
- 持久化是双份的:App 存
VEHICLE_MOBILE_NETWORK_SELECTION(纯 UI 缓存),平台存HMI_DATA_SIM_APN(权威值)。两边不一致时以平台为准(见流程二)。
4. 核心流程二:开页选中态校准
sequenceDiagram autonumber participant F as Fragment 生命周期 participant C as TeleCommunicationSelectController participant G as Settings.Global(App 缓存) participant T as TelephonyUtil participant P as 平台 getUserDefaultChannel F->>C: onResumeInternal() (:99) C->>G: 读 VEHICLE_MOBILE_NETWORK_SELECTION<br/>(缺省 -1) alt 缓存有效(≠ -1) C->>C: refreshMobileNetSelection(缓存值)<br/>先按缓存画 UI(:108-110) end C->>+T: ASYNC 线程 getDefaultChannel() (:112) T->>+P: 反射 getUserDefaultChannel() P-->>-T: 平台权威值 netIndex T-->>-C: netIndex alt netIndex ≠ 缓存值 C->>G: 更新 App 缓存(setSelectionToCache) C->>C: mUIHandler.post {<br/>refreshMobileNetSelection(netIndex) }<br/>(:120-123 注释:避开 RecyclerView 布局期 crash) else 一致 Note over C: 什么都不做 end
要点:先画缓存、再拿平台权威值校准,平台值是最终事实。所以「平台受理了但执行没切」 的场景下,下次开页 Tab 依然停在用户选的那项(因为平台受理层确实记了偏好)。
5. 核心流程三:ECALL 通话中禁用
flowchart TD Create[onCreateInternal :50] --> Reg[registerCallback :215] Reg --> Subs["遍历 getActiveSubscriptionInfoList()<br/>每个 sub 建一个专属 TelephonyManager(context, subId)"] Subs --> Cb["每个 manager 注册独立 TelephonyCallback<br/>(PreciseCallStateListener)"] Cb -.为什么这么绕.-> Why["fwk binder 对同一 callback 对象去重:<br/>多 manager 共享一个 callback 时,解注册只能释放最后一个,<br/>累积会 callback 数量超限(:44-47 注释)"] Cb --> State{onPreciseCallStateChanged} State --> Upd[updateEnableState :56] Upd --> Check{任一 SIM<br/>OFFHOOK / RINGING?} Check -- 是 --> Dis["preference.isEnabled=false<br/>所有 tab.isAvailable=false<br/>→ 整排置灰"] Check -- 否 --> En["恢复可点"] Dis -.置灰时点击.-> Dialog["onDisabledTabClicked :70<br/>弹 sim_change_fail_dialog"] Destroy[onDestroyInternal] --> Unreg["unregisterCallback :233<br/>逐对解注册+clear"]
6. Flavor 差异(三源码集)
flowchart LR subgraph Build["micarConnectionSettings flavor 源码集"] X["src/xcddif/.../TelephonyUtil.kt<br/>201 行 · 全功能"] F["src/f3dif/.../TelephonyUtil.kt<br/>201 行 · 与 xcddif 完全一致"] D["src/dcddif/.../TelephonyUtil.kt<br/>152 行 · 卫星方法名不同<br/>getDefaultChannel/setDefaultChannel 空桩"] end X --> XCD["XCD 平台(suiren 等)<br/>双 eSIM,功能完整"] F --> F3["F3 平台(fermi)<br/>功能完整"] D --> DCD["DCD 平台(watt/8295)<br/>单卡,无优先运营商功能<br/>连接页整组隐藏"]
注意:三份
TelephonyUtil还承担卫星通信的反射封装(setSatelliteEnable/isSatelliteEnable/isSupportSatelliteByDevice),类在mi/collinear/satellite包下, 「优先运营商」与「卫星通信」共用这个工具类,但互相独立。dcddif 连卫星方法名都不同 (METHOD_isSupportSatelliteByDevicevsMETHOD_IS_DEVICE_SUPPORTED),改它时三份都要看。
7. 数据存储双轨制
flowchart TB subgraph AppSide["App 侧(UI 缓存)"] K1["Settings.Global<br/>VEHICLE_MOBILE_NETWORK_SELECTION<br/>值:-1未知 / 0电信 / 1移动 / 2智能"] end subgraph PlatSide["平台侧(权威)"] K2["Settings.Global<br/>HMI_DATA_SIM_APN<br/>值:0=sim1 / 1=sim2 / 2=wisdom(默认)"] K3["Settings.Global<br/>DEFAULT_DATA_SIM_APN{apnNumber}<br/>(云端策略通道偏好)"] end K1 <-."开页时以 K2 校准 K1<br/>(onResumeInternal)".-> K2 K2 -.受理层写入.-> R["MultiInternetRouteController<br/>:402"] K3 -.策略下发写入.-> R2["MultiInternetRouteController<br/>:266"]
排查「UI 显示和实际网络不一致」时,先分别读这两个 key:
adb shell settings get global VEHICLE_MOBILE_NETWORK_SELECTION 和
adb shell settings get global HMI_DATA_SIM_APN——一致≠真切了(可能只是受理了),
不一致=平台连受理都没受理。