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
平台 APIbinder 通道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)

这张图的两个要害:

  1. 第 13 步的 false 被第 14 步吃掉——执行层拒绝(比如目标卡无服务)不会传导回 App, App 看到的永远是「受理成功」。这是 N801PROB-138847 的契约层根因(详见 04/05 篇)。
  2. 持久化是双份的: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_isSupportSatelliteByDevice vs METHOD_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——一致≠真切了(可能只是受理了), 不一致=平台连受理都没受理。