05 - 车辆接口层与 IPC

本篇概览

本篇聚焦 MiCarSettings 的「车辆接口层」——base/settingsVehicleLib 模块。这一层是整个 App 与真实车辆硬件之间的「翻译官」:它把上层 UI Controller 发出的语义化操作(“打开离开锁车”、“切换到运动模式”)翻译成 AOSP android.car.* API 调用,把车端上报的 CarPropertyValue 反向翻译成 UI 可以直接刷新的状态。

读完本篇,你能掌握:

  1. settingsVehicleLib 的目录结构和对外核心类(Manager、Controller 基类、配置字、属性集合);
  2. Car 实例是怎么拿到的、CarPropertyManager 是怎么注册/解注册监听的;
  3. CarConfigManager 怎么读取 CCP 配置字、为什么要分缓存和 skipCache;
  4. BaseVehicleTabPreferenceController / BaseVehicleImageTabPreferenceController / BaseVehicleNewSwitchPrefController 三个核心抽象基类,怎么把「CarProperty 回调 → UI 刷新」串成一条线;
  5. 一个真实的开关 subclass(WalkAwayLockControllerCreepSpeedPreferenceController)走的是怎样一条完整链路;
  6. dcd / xcd 两套平台是怎么在同一份源码、不同 framework.jar 下屏蔽差异的;
  7. 新增一个需要读写车辆属性的开关时,从零开始的数据链路。

阅读建议:第一遍顺着读,第二遍对照源码读。每节末尾「📌 新手记住」是高频被问到的考点。


1. 车辆接口层在哪、提供了什么

1.1 模块定位

base/settingsVehicleLib 是一个纯 Kotlin/Java 库模块,依赖 base/settingsBaseUi(提供 PreferenceController 抽象基类)和 base/settingsBaseLib。它向上为各业务页面(settingsPage/*)提供车辆能力的统一抽象,向下通过 AOSP android.car.* API 与 CarService(system_app 进程内的车控/车况服务)通信。

flowchart TB
    subgraph UI["业务页面层(settingsPage/*)"]
        Frag["Fragment & 子类 Controller<br/>如 WalkAwayLockController"]
    end
    subgraph VLIB["base/settingsVehicleLib(本篇主角)"]
        Base["抽象基类<br/>BaseVehicleNewSwitchPrefController<br/>BaseVehicleTabPreferenceController<br/>BaseVehicleImageTabPreferenceController"]
        CarPropMgr["CarPropertyMgrPreferenceController<br/>(中间层,桥接 PreferenceController 和车信号)"]
        SCM["SettingsCarPropertyManager<br/>VCM<br/>CarPropertyCacheManager"]
        CCM["CarConfigManager<br/>(CCP 配置字读取 + 缓存)"]
        LCM["LocalCarManager<br/>(Car 单例)"]
    end
    subgraph CAR["AOSP Car API & 车控服务"]
        Car["android.car.Car"]
        CPM["CarPropertyManager"]
        HAL["车控 HAL / 车端 ECU"]
    end
    Frag --> Base
    Base --> CarPropMgr
    CarPropMgr --> SCM
    SCM --> LCM
    SCM --> CPM
    LCM --> Car
    Car --> CPM
    CPM --> HAL
    CCM -. 读配置字 .-> SCM
    Frag -. 偶尔直接读配置字 .-> CCM

1.2 核心类清单(按调用方向从上到下)

实际读 base/settingsVehicleLib/src/main/java 下的源码,对外暴露的能力集中在以下几个文件,按职责分类:

文件(相对仓库根路径)职责
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java:54所有「需要读写车辆信号的 Controller」的抽象基类,实现 CarPropertyManagerCallback,对接 SettingsCarPropertyManager
…/miauto/vehicle/BaseVehicleNewSwitchPrefController.kt:18「开关型」Controller 抽象基类(继承上一行)
…/miauto/vehicle/BaseVehicleTabPreferenceController.kt:34「Tab 切换型」Controller 抽象基类
…/miauto/vehicle/BaseVehicleImageTabPreferenceController.kt:27「带图标的 Tab 切换型」Controller 抽象基类(与上面并行,独立继承 CarPropertyMgrPreferenceController
…/miauto/vehicle/SettingsCarPropertyManager.kt:25业务层真正使用的车信号管理器:读写 int/float/boolean/int[]/String/byte[]、注册回调、超时监控、本地缓存
…/miauto/vehicle/VehicleControlManager.java:43SettingsCarPropertyManager 内部的下层封装,直接持有 AOSP CarPropertyManager,按 propId 注册/解注册、按类型 set/get
…/miauto/vehicle/NewBaseCarServiceManager.java:12所有需要 Car.*Manager 的基类,封装「Car 生命周期 → 取目标 Manager」流程
…/miauto/vehicle/LocalCarManager.java:17进程内 Car 单例,调用 Car.createCar(...) 并维护生命周期 listener 链
…/miauto/vehicle/CarPropertyCacheManager.java:25跨 Controller 共享的 CarPropertyValue 缓存与 callback 分发器(一个 propId 多个 callback 时只注册一次到 server)
…/miauto/GlobalCarPropertyManager.kt:16全局 SettingsCarPropertyManager 入口(SoftReference 持有),供 CarConfigManager / 不属于具体页面的逻辑直接读信号
…/miauto/CarConfigManager.kt:48CCP 静态配置字(车型、硬件开关)查询 + 缓存
…/miauto/vehicle/MiCarPropertyIds.java所有车信号 propId 常量(dcd/xcd 两套 framework 通过这个文件的常量交叉编译)
…/miauto/VehiclePermanentListener.javaApp 启动时常驻的信号监听器,注册一批「全局关注信号」(驾驶模式、车主模式等)

📌 新手记住:业务 Controller 永远不会直接 new Car、不会直接 getCarManager(PROPERTY_SERVICE)。一切都要经过 CarPropertyMgrPreferenceControllerSettingsCarPropertyManagerVehicleControlManagerCarPropertyManager。这条链上任何一环都做了缓存、超时、异常兜底,绕过它就等于裸跑。


2. Car 实例从哪来:LocalCarManager 与生命周期

2.1 单例 + 懒加载

android.car.Car 是 AOSP 提供的客户端入口,本质上绑定到 system_app 内的 CarService。一个 App 进程只需要一个 Car 实例。settingsVehicleLib 把它收口在 LocalCarManager(单例):

// base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/LocalCarManager.java:82
public Car createCar(Context context, Car.CarServiceLifecycleListener statusChangeListener) {
    if (mCar == null) {
        synchronized (LocalCarManager.class) {
            if (mCar == null) {
                mCar = Car.createCar(context.getApplicationContext(), null,
                        Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, mLifecycleListener);
            }
        }
    }
    notifyStatusChange(statusChangeListener);   // 把调用方的 listener 也登记进链表
    return mCar;
}

几个要点:

  • 双重检查锁保证全进程只有一个 Car
  • Car.createCar(..., CAR_WAIT_TIMEOUT_WAIT_FOREVER, listener) 表示永远等CarService 上线,避免轮询;
  • mLifecycleListenerLocalCarManager.java:44)才是「主」listener,CarService 每次 ready/died 都会回调它,它再去遍历 mListeners 链表通知所有业务方;
  • 业务方调用 LocalCarManager.getInstance().createCar(ctx, ownListener) 时,自己的 listener 被加进 mListeners不会真的再去 Car.createCar 一次。

2.2 谁触发了第一次 createCar

App 启动时 MainThreadStartTask 在主线程做了三件事:

// app/src/main/java/com/android/car/settings/miauto/MainThreadStartTask.java:62
GlobalCarPropertyManager.init(mApplication);
...
VehiclePermanentListener.getInstance().init();
MiCarSettings.setConfigManager(CarConfigManager.INSTANCE);

GlobalCarPropertyManager.initGlobalCarPropertyManager.kt:23new 了一个 SettingsCarPropertyManager(context),后者的构造链路 SettingsCarPropertyManagerVehicleControlManagerNewBaseCarServiceManagerNewBaseCarServiceManager.java:23)就触发了第一次 LocalCarManager.getInstance().createCar(...)

也就是说:App 启动那一刻,Car 实例就被创建了,但 CarService 是否 ready 要等系统回调

2.3 「Car ready」之后做了什么

NewBaseCarServiceManager 把所有车服务管理器(这里是 CarPropertyManager)的获取统一抽象成模板方法:

// base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/NewBaseCarServiceManager.java:36
@Override
public void onLifecycleChanged(Car car, boolean ready) {
    ...
    if (ready) {
        carServiceManager.mCarServiceManager = (T) car.getCarManager(
                carServiceManager.getServiceName());   // 拿到 CarPropertyManager
    } else {
        carServiceManager.mCarServiceManager = null;    // 服务挂掉,置空
    }
    // 通知上层
    if (carServiceManager.mCarServiceListener != null) {
        if (carServiceManager.mCarServiceManager == null) {
            carServiceManager.mCarServiceListener.onCarServiceManagerReleased();
        } else {
            carServiceManager.mCarServiceListener.onCarServiceManagerPrepared();
        }
    }
}

VehicleControlManager.getServiceName() 返回 Car.PROPERTY_SERVICEVehicleControlManager.java:53),所以这里取到的就是 AOSP 的 CarPropertyManager

flowchart TD
    A["App 启动<br/>MainThreadStartTask"] --> B["GlobalCarPropertyManager.init<br/>new SettingsCarPropertyManager"]
    B --> C["new VehicleControlManager"]
    C --> D["NewBaseCarServiceManager 构造"]
    D --> E["LocalCarManager.createCar<br/>Car.createCar(WAIT_FOREVER)"]
    E --> F{"Car 实例<br/>(单例)"}
    F -.系统回调.-> G["CarServiceLifecycleListener<br/>onLifecycleChanged(ready)"]
    G --> H["car.getCarManager(PROPERTY_SERVICE)<br/>拿到 CarPropertyManager"]
    H --> I["OnCarServiceListener<br/>onCarServiceManagerPrepared"]
    I --> J["SettingsCarPropertyManager<br/>setCarManagerCallback 的回调"]
    J --> K["CarPropertyMgrPreferenceController<br/>onPropertyManagerPrepared<br/>registerPropertyCallback()"]

📌 新手记住:永远不要在 Controller 构造函数里去拿 Car 实例。Controller 构造时 CarService 多半还没 ready。正确的姿势是在 onCreateInternalnew SettingsCarPropertyManager(...)(基类已经做了),然后在 onPropertyManagerPrepared 回调里再去注册关心的 propId。


3. 配置字 CCP:CarConfigManager

3.1 什么是 CCP

CCP(Car Configuration Parameter)是出厂烧录到车端的硬件配置字,车辆整个生命周期不变。例如「这辆车是纯电还是增程」「有没有电动踏板」「充电口是国标还是欧标」「有没有调光天幕」「销往国内还是欧洲」。在小米座舱里,它们以 CarProperty 的形式暴露出来,propId 集中定义在 MiCarPropertyIds.CarConfig.*

CarConfigManagerCarConfigManager.kt:48)是一个 object(单例),把它们全部收口:

// base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/CarConfigManager.kt:110
fun getPropulsionTypeConfig(): Int {            // 动力类型
    return getIntProperty(
        MiCarPropertyIds.CarConfig.PROPULSION_TYPE_CONFIG,
        Driving.PropulsionTypeConfig.ELECTRIC   // 默认值
    )
}
fun isEnergyModeEnabled(): Boolean {            // 仅增程车支持能源模式切换
    return getPropulsionTypeConfig() == Driving.PropulsionTypeConfig.EXTENDED_RANGE_ELECTRIC
}

业务方永远只调 CarConfigManager.isXxxEnable(),不应该关心 propId 是多少。

3.2 配置来源

CarConfigManager 不直接读 CarService,而是经由 GlobalCarPropertyManager.getCarManager() 拿到一个全局共享的 SettingsCarPropertyManager 实例,然后调它的 getIntPropertyWithoutCache

// CarConfigManager.kt:92
val value = GlobalCarPropertyManager
    .getCarManager()
    .getIntPropertyWithoutCache(propId, default = INVALID_CONFIG_VAL)

最终走的是 SettingsCarPropertyManager.getProperty()VehicleControlManager.getProperty() → AOSP CarPropertyManager.getProperty(propId, areaId)

3.3 缓存与「服务不可用哨兵值」

CCP 是静态配置,重复 getProperty() 每次都是一次 Binder + HAL 调用,开销大。所以 CarConfigManager 默认开启缓存:

// CarConfigManager.kt:78
private fun getIntProperty(propId: Int, defaultPropVal: Int, skipCache: Boolean = false): Int {
    if (!skipCache) {
        mConfigCache[propId]?.let { return it }   // 命中缓存直接返回
    }
    // 用哨兵值 Int.MIN_VALUE 探测 CarService 是否可用
    val value = GlobalCarPropertyManager.getCarManager()
        .getIntPropertyWithoutCache(propId, default = INVALID_CONFIG_VAL)
    if (value == INVALID_CONFIG_VAL) {            // 服务不可用,不污染缓存
        return defaultPropVal
    }
    if (!skipCache) mConfigCache[propId] = value
    return value
}

这里有一个关键设计点(也是单元测试 CarConfigManagerTest.kt 反复验证的):用 Int.MIN_VALUE 作为「服务不可用」的哨兵值。

为什么不能直接用业务方的 default? 设想第一次查询时 CarService 还没连上,底层会静默返回你传进去的 default。如果直接把 defaultPropVal(比如「纯电」)写进缓存,后续 CarService 真正连上了,你的缓存里依然是「纯电」,整辆车都被当成纯电车,UI 全错。所以代码先用 INVALID_CONFIG_VAL 探一下:只有返回的不是哨兵值才认为这是真值,可以缓存;返回哨兵值就老老实实回退默认值,不写缓存,下一次还会重新去查。

CarConfigManagerTest.kt:97-133 把这个场景测了三遍:不可用时不缓存、不可用恢复后能取到真值、不可用时每次都重试。

3.4 哪些不能走缓存

凡是「会随用户操作实时变化」的属性,必须显式 skipCache = true,例如:

// CarConfigManager.kt:619  展车模式(用户可在 CarModeDialogActivity 切换)
fun isCarModeInShowRoom(): Boolean {
    return getIntProperty(Driving.CAR_MODE_STATUS, Driving.CarModeStatus.NORMAL, true) ==
            Driving.CarModeStatus.SHOWROOM
}
 
// CarConfigManager.kt:843  车主模式实时状态
fun getOwnerModeActiveState(): Int {
    return getIntProperty(MiCarPropertyIds.SafeModeCtrl.SAFE_MODE_DISPLAY_FOR_APP,
            CommonParams.TrueFalse.FALSE, true)
}

类头注释(CarConfigManager.kt:38-46)反复强调了这条铁律。CarConfigManagerTest.kt:157 单独测了 skipCache=true 每次都会真正去查 CarService。

📌 新手记住

  • 新增的 CCP 查询方法,先确认这真的是出厂配置字(车辆生命周期内不变),才能用默认缓存;
  • 只要是「会变的」属性(挡位、模式、车主模式状态),要么不走 CarConfigManager、要么必须 skipCache = true
  • CarConfigManager 提供了 clearCache(),OTA 升级、App 重启后调用,避免配置字变更后还读到旧值。

4. 业务 Controller 基类:从 PreferenceController 到车信号驱动

4.1 继承关系总览

settingsBaseUi 提供了 PreferenceController<V extends Preference>base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java:106),它和 AOSP Car Settings 保持一致,提供 onCreateInternal / onResumeInternal / onDestroyInternal 等生命周期模板方法。settingsVehicleLib 在它之上做了一层针对「车信号驱动」的扩展:

classDiagram
    class PreferenceController~V~ {
        <<settingsBaseUi>>
        +onCreateInternal()
        +onResumeInternal()
        +onDestroyInternal()
    }
    class CarPropertyManagerCallback {
        <<interface>>
        +onPropertyManagerPrepared()
        +onPropertyManagerReleased()
        +onPropertyChanged(CarPropertyValue)
        +onSetPropertyTimeout(propId, areaId)
        +onSetPropertyError(propId, areaId)
        +getTimeoutFlag(CarPropertyValue)
    }
    class CarPropertyMgrPreferenceController~V~ {
        #SettingsCarPropertyManager mCarPropertyManager
        #abstract getPropertyIdSet()
        #registerPropertyCallback()
        +onPropertyChanged(CarPropertyValue)
        #onHandlePropertyChange(CarPropertyValue, shouldUpdateUI)
        #setVehicleProperty(propId, val, areaId)
    }
    class BaseVehicleNewSwitchPrefController {
        +onHandlePropertyChange()
        +handlePreferenceChanged(preference, newValue)
        #abstract getPropertyIdSet()
        #isPropertyOpen(propVal)
    }
    class BaseVehicleTabPreferenceController {
        #abstract buildTabData()
        +onSelectTabChanged(tab)
        #onHandlePropertyChange()
        #updateTabByPropertyChange()
    }
    class BaseVehicleImageTabPreferenceController {
        #abstract buildTabData()
        #abstract propValToTabIndex()
        #abstract setCarProperty(tab)
    }
    class WalkAwayLockController {
        +getPropertyIdSet() =WALK_AWAY_LOCK
    }
    class CreepSpeedPreferenceController {
        +getPropertyIdSet() =CREEP_SPEED_STATUS
        +buildTabData()
    }

    PreferenceController <|-- CarPropertyMgrPreferenceController
    CarPropertyManagerCallback <|.. CarPropertyMgrPreferenceController
    CarPropertyMgrPreferenceController <|-- BaseVehicleNewSwitchPrefController
    CarPropertyMgrPreferenceController <|-- BaseVehicleTabPreferenceController
    CarPropertyMgrPreferenceController <|-- BaseVehicleImageTabPreferenceController
    BaseVehicleNewSwitchPrefController <|-- WalkAwayLockController
    BaseVehicleTabPreferenceController <|-- CreepSpeedPreferenceController

4.2 中间层:CarPropertyMgrPreferenceController

这一个抽象类是整个接口层的「脊梁」(CarPropertyMgrPreferenceController.java:54)。它做了五件事:

  1. 持有 SettingsCarPropertyManager:在 onCreateInternalnew 出来并绑到 Fragment 生命周期(CarPropertyMgrPreferenceController.java:513)。
  2. 声明关心的 propId 集合:抽象方法 getPropertyIdSet() 由子类实现。
  3. 实现 CarPropertyManagerCallbackonPropertyManagerPrepared 时去注册监听、onPropertyChanged 时分发给子类的 onHandlePropertyChange
  4. 统一封装 set 信号setVehicleProperty(propId, value, areaId, isSync)CarPropertyMgrPreferenceController.java:620),子类不需要管 Int/Float/IntArray/List<CarPropertyValue> 的分支判断。
  5. 管理超时 / 错误回退:信号下发后启动一个超时计时器,超时则 restorePropertyToPreviousCarPropertyMgrPreferenceController.java:393)把 UI 状态回滚到缓存值。

4.3 三种业务基类

基类适用场景关键抽象方法
BaseVehicleNewSwitchPrefControllerBaseVehicleNewSwitchPrefController.kt:18「开关」(如离开锁车、电动踏板、自动落锁)getPropertyIdSet()isPropertyOpen(propVal)needSecondConfirm()
BaseVehicleTabPreferenceControllerBaseVehicleTabPreferenceController.kt:34「文字 Tab」(如蠕行速度档位、能量模式、转向手感)buildTabData()propValToTabIndex()getPropValueByTab()
BaseVehicleImageTabPreferenceControllerBaseVehicleImageTabPreferenceController.kt:27「图标 Tab」(如驱动形式选择、单位选择)buildTabData()propValToTabIndex()setCarProperty(tab)

BaseVehicleImageTabPreferenceControllerBaseVehicleTabPreferenceController 是并行的两个抽象,都直接继承 CarPropertyMgrPreferenceController,不要误以为是父子关系(见 BaseVehicleImageTabPreferenceController.kt:25-26 的注释)。这是为了分别支持 NewTabPreference(文字)和 ImageTabPreference(图标)两套 Preference 视图。

4.4 「点击 → 下发」的链路

WalkAwayLockController(开关)为例,用户拨动开关:

// settingsPage/micarLockSettings/.../lock/WalkAwayLockController.kt:10
class WalkAwayLockController(...) : BaseVehicleNewSwitchPrefController(...) {
    override fun getPropertyIdSet(): ArraySet<Int> {
        return ArraySet<Int>().apply { add(MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK) }
    }
}

子类只写了 7 行代码,剩下的全是基类做的:

  1. 用户拨动 → MiCarNewSwitchPreference 触发 handlePreferenceChanged(preference, newValue)
  2. BaseVehicleNewSwitchPrefController.handlePreferenceChangedBaseVehicleNewSwitchPrefController.kt:128)判断是否需要二次确认,不需要则调 setCarProperty(isChecked)
  3. setCarPropertyBaseVehicleNewSwitchPrefController.kt:189)调 setVehicleProperty(obtainSetPropertyId(), getPropertyVal(isChecked), areaId)
  4. CarPropertyMgrPreferenceController.setVehiclePropertyCarPropertyMgrPreferenceController.java:620)调 mCarPropertyManager.setCarProperty(...)
  5. SettingsCarPropertyManager.setCarPropertySettingsCarPropertyManager.kt:423)按 propVal 类型(Int/Float/Array/List)分发,启动超时计时器;
  6. VehicleControlManager.setIntPropertyThreadPoolUtils.ASYNC.execute(...)VehicleControlManager.java:115),把 setIntProperty 异步发到 AOSP CarPropertyManager

4.5 「信号变化 → UI 刷新」的链路(核心)

车端把开关真正打开后,会上报一个 CarPropertyValue。从 Binder 到 UI 的完整回调链路:

sequenceDiagram
    autonumber
    participant ECU as 车端 ECU
    participant CPM as AOSP<br/>CarPropertyManager
    participant Cache as CarPropertyCacheManager<br/>(单例)
    participant SCPM as SettingsCarPropertyManager<br/>(per-Controller)
    participant CPMC as CarPropertyMgrPreferenceController<br/>(基类)
    participant Sub as 子类 Controller<br/>(如 WalkAwayLockController)
    participant Pref as Preference View

    ECU->>CPM: 属性变化上报
    CPM->>Cache: onChangeEvent(CarPropertyValue)
    Cache->>Cache: 缓存 propId#areaId -> value
    Note over Cache: 同 propId 多个 callback 时<br/>只注册一次到 server
    Cache->>SCPM: 内部 callback 分发<br/>(mInternalCarPropertyEventCallback)
    %% SCM as SettingsCarPropertyEventCallback (原为孤立备注行, 非 sequence 语句, 注释化以通过 mermaid 解析)
    SCPM->>SCPM: SettingsCarPropertyEventCallback.onChangeEvent
    SCPM->>SCPM: cachePropVal / processTimeout
    SCPM->>CPMC: mCallback.onPropertyChanged(value)<br/>(CarPropertyManagerCallback)
    CPMC->>CPMC: shouldIgnorePropertyFeedback?<br/>shouldUpdateUIByPropVal?
    CPMC->>CPMC: 依赖信号映射?<br/>(IVehiclePerformFeature.needMapReceiveDependProperty)
    CPMC->>Sub: onHandlePropertyChange(value, shouldUpdateUI)
    Sub->>Sub: preference.setChecked(isPropertyOpen(propVal))
    Sub->>Pref: preference.post { notifyDataChanged() }

链路上每一步的源码位置:

步骤源码位置
全局 callback 注册(同 propId 只注册一次)CarPropertyCacheManager.java:159 registerPropertyCallback
内部 callback 接收并分发CarPropertyCacheManager.java:61 onChangeEvent
转发到 per-Controller 的 callbackSettingsCarPropertyManager.kt:662 SettingsCarPropertyEventCallback.onChangeEvent
基类收到并做过滤CarPropertyMgrPreferenceController.java:346 onPropertyChanged
子类刷新 UIBaseVehicleNewSwitchPrefController.kt:48 onHandlePropertyChange

4.6 几个值得记住的细节

  • 同 propId 共享注册CarPropertyCacheManager 是个单例,记录「propId → callback 集合」。即使 10 个 Controller 都关心同一个 propId,对底层 CarPropertyManager.registerCallback 也只调一次(CarPropertyCacheManager.java:192)。这避免了多 Controller 监听同一信号时的 N 次 Binder 注册。
  • areaId 过滤:很多信号分区域(左前、右前车门等)。BaseVehicleNewSwitchPrefController.onHandlePropertyChange 第一步就是检查 areaIdBaseVehicleNewSwitchPrefController.kt:53),不是自己关心区域直接 return,否则会出现「发一个信号两个开关同时响应」。
  • shouldUpdateUIByPropVal:某些信号会上报「保留值 / 预留值」,此时 UI 状态不应跳变,只置灰。子类 override isValidPropVal 即可(BaseVehicleNewSwitchPrefController.kt:82)。
  • 中间态(loading):开关切换后到底端再回包,期间会有几百毫秒。子类 override needIntermediateState() = true 并实现 isInLoadingState,开关就会进入 loading 态(BaseVehicleNewSwitchPrefController.kt:64)。
  • 下行/上行信号不一致:少数业务(如 CreepSpeedPreferenceController)下行用一个 propId、上行收到的是「错误码」依赖信号,靠 needMapReceiveDependProperty() = true 把依赖信号映射成主信号再刷新(CarPropertyMgrPreferenceController.java:360-380CreepSpeedPreferenceController.java:48)。

📌 新手记住:写一个新的车控 Controller,95% 的情况只需要实现 getPropertyIdSet()。其他能力(缓存、超时、错误回退、areaId 过滤、依赖信号映射、二次确认、中间态)全是基类已经做好的,按需 override 即可。先把基类读一遍,再动手写。


5. dcd / xcd:平台差异是怎么屏蔽的

5.1 两套平台是什么

小米座舱目前并存两套底层平台:

  • dcd:早期平台,framework / android.car.jar 来源于 framework_intermediates/javalib_watt_250721.jar
  • xcd:新平台,framework / android.car.jar 来源于 framework_intermediates/xcd_javalib_1129.jar
  • global:海外版本,编译期复用 xcd 的 framework,资源/逻辑有独立 sourceSet。

两套平台的 mi.car.config.*android.car.*符号表不同(propId 数值、枚举名、部分方法签名都有差异)。

5.2 屏蔽手段:编译期 jar 切换,不是源码抽象

一个非常容易踩坑的认知:很多项目用「抽象接口 + 两套 flavor 实现」屏蔽平台差异,但 settingsVehicleLib 没有这样做。读 base/settingsVehicleLib/src 目录,只有 main/test/ 两个源码集,没有 dcddif/xcddif/ 这些 flavor 目录里有任何 java 文件

虽然 build.gradle 声明了 flavor:

// base/settingsVehicleLib/build.gradle:10
productFlavors {
    dcd { dimension "miPlatform" }
    xcd { dimension "miPlatform" }
    global { dimension "miPlatform" }
}
sourceSets {
    dcd { java.srcDirs = ['src/main/java', 'src/dcddif/java'] }     // 这个目录是空的
    xcd { java.srcDirs = ['src/main/java', 'src/xcddif/java'] }     // 这个目录也是空的
}

src/dcddifsrc/xcddif 目录并不存在或为空。真正的差异屏蔽发生在build.gradle 的编译参数注入

// build.gradle:113
tasks.withType(JavaCompile) {
    if (options.bootstrapClasspath != null) {
        if (currentFlavor.contains("Dcd")) {
            newFileList.add(new File(rootProject.ext.jarLib.DCD_FRAMEWORK_JARPATH))
        } else if (currentFlavor.contains("Xcd") || currentFlavor.contains("Global")) {
            newFileList.add(new File(rootProject.ext.jarLib.XCD_FRAMEWORK_JARPATH))
        }
        options.bootstrapClasspath = files(newFileList.toArray())
    }
}

加上 dependencies.gradle:18-25 给每个 flavor 配 xxxCompileOnly 的 framework / car jar:

deps.add("xcdCompileOnly",  files(rootProject.xcd_framework_lib_path))
deps.add("globalCompileOnly", files(rootProject.xcd_framework_lib_path))
deps.add("dcdCompileOnly",  files(rootProject.dcd_framework_lib_path))
deps.add("xcdCompileOnly",  files(rootProject.xcd_car_ui_lib_path))
...

jar 路径在 constants.gradle:30-45 定义:

dcd_frameworkJarPath = ".../framework_intermediates/javalib_watt_250721.jar"
xcd_frameworkJarPath = ".../framework_intermediates/xcd_javalib_1129.jar"
dcd_car_ui_lib_path  = ".../android.car_intermediates/android.car_watt0911.jar"
xcd_car_ui_lib_path  = ".../android.car_intermediates/xcd_android.car_1129.jar"

结论:同一份 settingsVehicleLib 源码,在编译 dcd flavor 时被链接到 dcd 的 framework.jar;编译 xcd/global flavor 时被链接到 xcd 的 framework.jar。源码里写的 MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK = 0xXXXXXX 这个常量的真实数值由 jar 决定,两套 jar 给的值不一样,但源码字面量是一样的。这就是「源码不变、平台 jar 切换」的屏蔽方式。

5.3 这带来的约束

  1. 不要在源码里写魔法数字,必须用 mi.car.config.* 提供的符号常量(如 Driving.PropulsionTypeConfig.EXTENDED_RANGE_ELECTRIC),否则一套源码跑不通两套平台。
  2. 不要在 settingsVehicleLib 里写 if (flavor == dcd) 这种判断——这一层对平台无感。真正有平台差异的业务,写到 app 模块的 src/dcddif/javasrc/xcddif/java(这两个目录在 app 模块下是真实存在的)。
  3. 新增 propId 时,确认两套 MiCarPropertyIds 都有对应的常量定义,否则另一套编不过。
classDiagram
    class MiCarPropertyIds {
        <<来自 framework.jar / android.car.jar>>
        +int WALK_AWAY_LOCK
        +int CREEP_SPEED_STATUS
        +int PROPULSION_TYPE_CONFIG
    }
    class DCDJar {
        dcd_javalib_250721.jar
        WALK_AWAY_LOCK = 0x12340001
    }
    class XCDJar {
        xcd_javalib_1129.jar
        WALK_AWAY_LOCK = 0x56780001
    }
    class SettingsVehicleLibSource {
        同一份源码
        MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK
    }
    DCDJar ..> MiCarPropertyIds : 编译 dcd 时
    XCDJar ..> MiCarPropertyIds : 编译 xcd/global 时
    SettingsVehicleLibSource --> MiCarPropertyIds : 只用符号引用

📌 新手记住:在 settingsVehicleLib 写代码,永远只用符号常量,永远假设这份代码会在 dcd 和 xcd 上各编一次。如果业务上真有平台差异,去 app 模块的 src/dcddifsrc/xcddif 里写 subclass,不要污染 vehicle 库。


6. IPC:除了 CarPropertyManager 之外的通道

CarPropertyManager 是主通道,但项目里还有一些 AIDL 直连:

settingsPage/settingsSystem/aidl/com/micar/SpaceMonitorSvc/IStorageCleaner.aidl
settingsPage/micarChargeSettings/src/main/java/com/micar/scene/aidl/IBALService.aidl
settingsPage/micarChargeSettings/src/main/java/com/micar/scene/aidl/IBookChargingMsgListener.aidl
settingsPage/micarChargeSettings/src/main/java/com/micar/scene/aidl/BookChargingMsg.aidl
base/settingsBaseLib/src/main/aidl/com/mi/car/account/IAccountConfirmResponse.aidl
base/settingsBaseLib/src/main/java/com/android/license/master/ILicenseMasterInterface.aidl

它们处理「无法用 CarProperty 表达」的复杂交互(预约充电消息流、账号同步、License 校验、存储清理),都是标准的 AIDL bindService 模式,不在本篇展开。但要注意:新增功能优先想能不能用 CarProperty 表达,能就走 CarPropertyMgrPreferenceController,避免又引入一条独立 IPC 链路。

至于 android.car.* 的其他 Manager(CarPowerManagerCarCabinManagerCarDiagnosticManager 等),项目里也有零散使用(如 CarHudControlManager.kt:63CarUserPositionManager.kt:36CarUxRestrictionsHelper.java:108),它们各自 Car.createCar 拿到 Car 实例后 getCarManager(XXX_SERVICE)。如果新增功能要用到,参考 NewBaseCarServiceManager 模板:继承它、override getServiceName()、复用 LocalCarManager 单例,不要另起 Car.createCar

📌 新手记住

  • 优先 CarPropertyManager(统一经过缓存、超时、生命周期管理);
  • 其次其他 Car.*Manager(继承 NewBaseCarServiceManager);
  • 最后才考虑自定义 AIDL。

7. 实战:新增一个「读写车辆属性的开关」全链路

假设产品提需求:「新增 XXX 功能开关,依赖车型配置字 XXX_CONFIG,用户可开关,开关状态对应 propId XXX_SWITCH_STATUS」。

第 1 步:确认配置字

CarConfigManager.kt 加查询方法:

fun isXxxEnable(): Boolean {
    return getIntProperty(
        MiCarPropertyIds.CarConfig.XXX_CONFIG,                  // 配置字 propId
        Body.XxxConfig.NONE                                     // 默认值(来自 mi.car.config.Body)
    ) == Body.XxxConfig.EQUIPPED
}

如果 XXX_CONFIG 是动态变化的(罕见),记得 skipCache = true

第 2 步:注册 propId 常量

确认 MiCarPropertyIds 中已有 XXX_SWITCH_STATUS(dcd / xcd 两套 jar 都要支持)。如果没有,联系车控同事补 jar。

第 3 步:写 Controller

// settingsPage/xxx/src/main/java/.../XxxSwitchController.kt
class XxxSwitchController(
    context: Context?, preferenceKey: String?,
    fragmentController: FragmentController?, uxRestrictions: CarUxRestrictions?
) : BaseVehicleNewSwitchPrefController(context, preferenceKey, fragmentController, uxRestrictions) {
 
    override fun getPropertyIdSet(): ArraySet<Int> =
        ArraySet(listOf(MiCarPropertyIds.XxxCtrl.XXX_SWITCH_STATUS))
 
    override fun isPropertyOpen(propVal: Int): Boolean =
        propVal == CommonParams.OnOff.ON     // 视实际信号定义
 
    override fun needSecondConfirm(): Boolean = true  // 业务要求二次确认
}

第 4 步:可见性控制

在 Fragment 里通过 CarPropertyMgrPreferenceController 提供的子项可见性机制,或者在外层 Controller 的 checkAvailability() 里调 CarConfigManager.isXxxEnable() 决定显示与否。

第 5 步:XML 配置

<MiCarNewSwitchPreference
    android:key="@string/pref_key_xxx_switch"
    android:title="@string/xxx_title"
    settings:controller="com.android.car.settings.miauto.xxx.XxxSwitchController"/>

第 6 步:链路自检(无需车也可以验证)

打开开关后的完整数据流:

flowchart LR
    A["用户拨开关"] --> B["MiCarNewSwitchPreference<br/>handlePreferenceChanged"]
    B --> C["XxxSwitchController<br/>handlePreferenceChanged (基类)"]
    C --> D["setCarProperty(true)<br/>→ setVehicleProperty(XXX_SWITCH_STATUS, ON, areaId)"]
    D --> E["CarPropertyMgrPreferenceController<br/>setVehicleProperty"]
    E --> F["SettingsCarPropertyManager<br/>setCarProperty (启动超时)"]
    F --> G["VehicleControlManager<br/>setIntProperty (异步线程)"]
    G --> H["AOSP CarPropertyManager<br/>setProperty"]
    H --> I["车端 ECU"]
    I -.上报.-> J["XXX_SWITCH_STATUS = ON"]
    J --> K["CarPropertyCacheManager<br/>onChangeEvent"]
    K --> L["SettingsCarPropertyManager<br/>SettingsCarPropertyEventCallback"]
    L --> M["CarPropertyMgrPreferenceController<br/>onPropertyChanged"]
    M --> N["XxxSwitchController<br/>onHandlePropertyChange"]
    N --> O["preference.setChecked(true)<br/>UI 最终刷新"]

如果你做完后开关「点了不生效」「状态不刷新」「切换后弹回」,按这条链路从两头查:日志里搜 VehicleControlManager 看 set 是否打印,搜 CarPropertyCacheManager 看上报是否到达,搜自己的 Controller 名字看 callback 是否被分发。

📌 新手记住:新增车控功能的开发量 = 1 个 Controller 子类(10~30 行) + XML 配置 + 可选的 CarConfigManager 查询方法。不要去重写基类,不要去 new Car,不要去手写 registerCallback


附录:关键文件速查表

主题文件路径
Car 单例base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/LocalCarManager.java
Car 生命周期抽象…/vehicle/NewBaseCarServiceManager.java
CarPropertyManager 封装…/vehicle/VehicleControlManager.java
业务级车信号管理…/vehicle/SettingsCarPropertyManager.kt
跨 Controller callback 缓存…/vehicle/CarPropertyCacheManager.java
全局 Manager 入口…/miauto/GlobalCarPropertyManager.kt
CCP 配置字…/miauto/CarConfigManager.kt
CCP 配置字单测base/settingsVehicleLib/src/test/java/com/android/car/settings/miauto/CarConfigManagerTest.kt
Controller 中间层…/miauto/common/CarPropertyMgrPreferenceController.java
开关基类…/vehicle/BaseVehicleNewSwitchPrefController.kt
Tab 基类…/vehicle/BaseVehicleTabPreferenceController.kt
图标 Tab 基类…/vehicle/BaseVehicleImageTabPreferenceController.kt
Feature 接口…/carproperty/IVehiclePerformFeature.java
全局常驻监听…/miauto/VehiclePermanentListener.java
flavor jar 注入build.gradle:100-136dependencies.gradle:18-29constants.gradle:30-46
App 启动初始化app/src/main/java/com/android/car/settings/miauto/MainThreadStartTask.java:62