后屏投影:从信号到显示的全链路

本篇是知识库的核心——回答「一个车辆信号是怎么变成屏幕上的 Tab 状态的?用户点一下 Tab 又发生了什么?」 整合自信号收发链路、UI 渲染、ModeTab 业务三份调研。

〇、数据流总览

后屏投影的信号流是双向 + 分离的:

flowchart LR
    subgraph 车端["车端 (CAN 总线)"]
        HW[幕布硬件/投影仪/座椅]
    end
    subgraph 信号层["信号层"]
        AOSP["AOSP CarPropertyManager"]
        CPCM["CarPropertyCacheManager<br/>全局缓存+分发"]
        SCPM["SettingsCarPropertyManager<br/>过滤+超时监控"]
    end
    subgraph 业务层["业务层"]
        CPP["CarPropertyMgrPreferenceController<br/>忽略/可见性"]
        BASE["BaseVehicleImageTabPreferenceController<br/>Tab三态计算"]
        CTRL["RearProjectionModeTabController<br/>业务翻译"]
    end
    subgraph UI层["UI 层"]
        PREF["ImageTabPreference"]
        VIEW["ThemeItemView"]
    end

    HW <-.上行状态.-> AOSP -- PROJECTION_SCREEN_STATUS --> CPCM --> SCPM --> CPP --> BASE --> CTRL --> PREF --> VIEW
    CTRL --下行命令 SCREEN_CONTROL_COMMAND--> SCPM --> AOSP -.指令.-> HW

两个方向用不同信号(详见 05-核心类拆解/01-RearProjectionModeTabController.md §3):

  • 上行(读):PROJECTION_SCREEN_STATUS(8 种状态值)
  • 下行(写):SCREEN_CONTROL_COMMAND(3 种命令值)

一、上行链路:车端状态 → Tab 显示

1.1 注册阶段(页面打开时)

sequenceDiagram
    autonumber
    participant Ctrl as ModeTabController
    participant Base as BaseVehicleImageTab
    participant CPP as CarPropertyMgrPref
    participant SCPM as SettingsCarPropertyManager
    participant VCM as VehicleControlManager
    participant CPCM as CarPropertyCacheManager
    participant AOSP as AOSP CarPropertyManager

    Ctrl->>Base: onPostCreateInternal()
    Base->>Base: onViewBindedFinish → registerPropertyCallback()
    CPP->>SCPM: registerPropertyCallback(getObservedPropertyIdSet())
    Note over SCPM: 观察集合 = 主信号 + 5个依赖信号
    SCPM->>VCM: 逐 propId registerPropertyCallback(dispatcher)
    VCM->>CPCM: registerPropertyCallback(mgr, callback, propId)
    CPCM->>AOSP: registerCallback(internalCb, propId, rate)
    Note over CPCM: 首次注册时 callbackByCache<br/>用缓存值同步触发一次

观察集合(ModeTabController.kt:116-123)包含 1 个主信号 + 5 个依赖信号,全部向 AOSP 注册回调。首次注册时 CarPropertyCacheManager 用缓存值同步触发一次回调,避免等 binder 回放——这是页面打开就能立刻显示当前幕布状态的关键。

双触发竞速注册(对抗审查厘清):registerPropertyCallback() 实际有两条触发路径竞速执行——

  • 路径 A:onPostCreateInternal → setCarManagerCallback → CarService 就绪回调 → onPropertyManagerPrepared → registerPropertyCallback
  • 路径 B:onPostCreateInternal → setPreferenceViewBindedListener → View 绑定完成 → registerPropertyCallback

两者目标相同(注册观察集合),由 CarPropertyMgrPreferenceControllermHasRegistered 守护(:275-278)先到者生效、后到者忽略。这是为应对「CarService 与 View 谁先就绪」不确定性的双保险,非矛盾

1.2 运行时回包分发

sequenceDiagram
    autonumber
    participant Car as 车端
    participant AOSP as AOSP CarPropertyManager
    participant CPCM as CarPropertyCacheManager
    participant SCPM as SettingsCarPropertyManager
    participant CPP as CarPropertyMgrPref
    participant Base as BaseVehicleImageTab
    participant Ctrl as ModeTabController
    participant Pref as ImageTabPreference
    participant View as ThemeItemView

    Car->>AOSP: 幕布状态变化(如展开中)
    AOSP->>CPCM: onChangeEvent(value)
    CPCM->>CPCM: 写缓存 key=propId#areaId
    CPCM->>SCPM: 遍历该 propId 的 callback 集合,逐个通知
    SCPM->>SCPM: cachePropVal + 检查是否在观察集合
    SCPM->>CPP: onPropertyChanged(value)
    CPP->>CPP: shouldIgnorePropertyFeedback? (回弹忽略判断)
    CPP->>Base: onHandlePropertyChange(value, shouldUpdateUI)
    Base->>Base: Guard: featureIgnore / areaId校验 / 可用性
    Base->>Base: updateTabByPropertyChange(propVal, isAvailable)
    Note over Base: 遍历 Tab 计算<br/>isAvailable/mIsSelected/isProcessing
    Base->>Ctrl: (子类 onHandlePropertyChange 处理过渡态 toast)
    Base->>Pref: preference.notifyDataChanged()
    Pref->>View: onBindViewHolder 重新绑定
    Note over View: isAvailable→灰/亮<br/>mIsSelected→高亮<br/>isProcessing→转圈

1.3 分发链路的”三道关卡”

信号从底层上来,经过层层把关才到 UI:

关卡位置作用
观察集合过滤SettingsCarPropertyManager只通知 getObservedPropertyIdSet() 内的信号,其余打 w 日志丢弃
回弹忽略CarPropertyMgrPreferenceControllershouldIgnorePropertyFeedback 判断是否忽略本次回包(配合超时回退)
areaId 校验BaseVehicleImageTab:118仅 Tab 主信号校验,areaId 不符丢弃(后屏投影用 DEFAULT_AREA_ID=0)

1.4 Tab 三态的计算(BaseVehicleImageTab:151-177)

信号值最终被压成 Tab 的三个布尔状态:

flowchart TD
    P["上行 propVal<br/>如 UNFOLD_TO_PROJECTION_STATUS"] --> A["propValToTabIndex(val)<br/>→ Tab 索引 1"]
    A --> B["遍历每个 Tab:"]
    B --> C1["isAvailable =<br/>isPropAvailable && isTabDependencyAvailable(tab)"]
    B --> C2["mIsSelected =<br/>(tabIndex == index)"]
    B --> C3["isProcessing =<br/>isSelected && isTabProcessing(tab, val)"]
    C1 --> D["ThemeItemView.setEnabled/setSelected/setProcessing"]

以「展开到投影中」(UNFOLD_TO_PROJECTION_STATUS)为例:

  • propValToTabIndex → 1(投影 Tab)
  • 投影 Tab:mIsSelected=trueisProcessing=true(因为 isTabProcessing 对过渡态返回 true)
  • 其他 Tab:mIsSelected=falseisProcessing=false
  • UI:投影 Tab 高亮 + 转圈

回包到稳态 PROJECTION_STATUS 时,isTabProcessing 返回 false → 转圈消失,只剩高亮。


二、下行链路:用户点击 → 车端执行

2.1 完整时序(含即时反馈 + 回弹确认 + 超时回退)

sequenceDiagram
    autonumber
    participant U as 用户
    participant Pref as ImageTabPreference
    participant Base as BaseVehicleImageTab
    participant Ctrl as ModeTabController
    participant CPP as CarPropertyMgrPref
    participant SCPM as SettingsCarPropertyManager
    participant VCM as VehicleControlManager
    participant AOSP as AOSP CarPropertyManager
    participant Car as 车端
    participant Handler as 超时Handler

    U->>Pref: 点击「投影」Tab
    Pref->>Base: onSelectTabChanged(projectionTab)
    Base->>Base: 投影Tab.isProcessing=true (即时loading)
    Base->>Ctrl: setCarProperty(tab)
    Ctrl->>CPP: setVehicleProperty(SCREEN_CONTROL_COMMAND, PROJECTION, areaId)
    CPP->>SCPM: setCarProperty(propId, val, areaId, isSync=false)
    SCPM->>VCM: setIntProperty(propId, val, areaId)
    VCM->>AOSP: mCarServiceManager.setIntProperty(...) (异步线程池)
    AOSP->>Car: 下发指令到 CAN 总线

    par 并行: 超时监控
        SCPM->>Handler: startTimeOutMonitor (幕布超时 750ms)
    and 正常回弹
        Car-->>AOSP: 执行完成,状态变化
        AOSP-->>SCPM: onChangeEvent(UNFOLD_TO_PROJECTION_STATUS)
        SCPM->>Handler: removeMessages (取消超时)
        SCPM->>CPP: onPropertyChanged
        CPP->>Ctrl: onHandlePropertyChange (走上行链路刷新 UI)
    end

    Note over Ctrl: 稳态 PROJECTION_STATUS 到达<br/>isProcessing→false, 高亮保留

2.2 即时 loading 反馈(不等回包)

用户点击瞬间,基类 onSelectTabChanged(BaseVehicleImageTab:211-215)立即给点击的 Tab 打 isProcessing=true:

flowchart LR
    A[点击 Tab] --> B[立即 isProcessing=true<br/>UI 转圈]
    B --> C[下发命令]
    C --> D{回包到达?}
    D -->|是| E[isTabProcessing 判定<br/>过渡态保持/稳态清除]
    D -->|超时| F[restorePropertyToPrevious<br/>UI 回退]

这样用户点完立刻看到反馈,不会觉得”没反应”。

2.3 超时监控与回退 ⭐

下发后会启动超时计时器(SettingsCarPropertyManager.setCarProperty:476):

  • 映射超时 propId:SCREEN_CONTROL_COMMAND(命令)→ PROJECTION_SCREEN_STATUS(状态)用于监听回弹(:542)。
  • 超时时长:默认 500ms,幕布类信号 750ms(机械动作慢,:543)。
  • 正常回弹:removeMessages 取消计时器(:181)。
  • 超时未回弹:handlePropertyTimeoutonSetPropertyTimeoutrestorePropertyToPrevious(:125)→ UI 回退到下发前状态

这个机制保证了”点了但车没响应”时,UI 不会一直停在 loading,而是自动回退,避免误导用户。

2.4 回弹忽略(shouldIgnorePropertyFeedback)

下行下发后,CarPropertyMgrPreferenceController 可能忽略某些回包(:349),配合超时机制做状态恢复。这是防止”下发→回弹→又回弹”抖动的保护。


三、异常态链路

除了正常上下行,还有几条异常信号链路(详见 07-异常态与依赖信号.md):

flowchart TD
    subgraph 依赖信号["依赖信号 (决定 Tab 可用性)"]
        D1[OFF_MODE_NOT_AVAILABLE_REASON]
        D2[PROJECTOR_NOT_AVAILABLE_REASON]
        D3[SCREEN_NOT_AVAILABLE_REASON]
    end
    subgraph TTS信号["TTS 信号 (安全提示)"]
        T1[ANTI_COLLISION_OR_ANTI_PINCH]
        T2[SEAT_LINKAGE_FAILED]
    end
    D1 & D2 & D3 -->|变化| D[onHandlePropertyChange<br/>super前主动读主信号重算可用性]
    D --> E[Tab enable/disable 刷新]
    T1 -->|REQUEST| F1[toast 幕布受阻]
    T2 -->|REQUEST| F2[toast 座椅调节失败]

依赖信号变化时,因为基类 notifyUIByPropertyChange 对非主信号默认不刷新 UI,所以 ModeTabController.onHandlePropertyChange(:164-181)抢在 super 之前用主信号缓存值重算 Tab 可用性——这是整个链路里最”hack”的一处补丁。


四、一句话总结

上行:车端信号 → AOSP → CarPropertyCacheManager(缓存+分发) → SettingsCarPropertyManager(过滤) → CarPropertyMgrPreferenceController(忽略判断) → BaseVehicleImageTab(三态计算) → ImageTabPreferenceThemeItemView(灰/亮/转圈)。

下行:点击 → onSelectTabChanged(即时 loading) → setCarPropertySCPM(超时监控) → AOSP → 车端;回弹走上行刷新,超时则 restorePropertyToPrevious 回退。