05 - 车辆接口层与 IPC
本篇概览
本篇聚焦 MiCarSettings 的「车辆接口层」——base/settingsVehicleLib 模块。这一层是整个 App 与真实车辆硬件之间的「翻译官」:它把上层 UI Controller 发出的语义化操作(“打开离开锁车”、“切换到运动模式”)翻译成 AOSP android.car.* API 调用,把车端上报的 CarPropertyValue 反向翻译成 UI 可以直接刷新的状态。
读完本篇,你能掌握:
settingsVehicleLib的目录结构和对外核心类(Manager、Controller 基类、配置字、属性集合);Car实例是怎么拿到的、CarPropertyManager是怎么注册/解注册监听的;CarConfigManager怎么读取 CCP 配置字、为什么要分缓存和 skipCache;BaseVehicleTabPreferenceController/BaseVehicleImageTabPreferenceController/BaseVehicleNewSwitchPrefController三个核心抽象基类,怎么把「CarProperty 回调 → UI 刷新」串成一条线;- 一个真实的开关 subclass(
WalkAwayLockController、CreepSpeedPreferenceController)走的是怎样一条完整链路; - dcd / xcd 两套平台是怎么在同一份源码、不同 framework.jar 下屏蔽差异的;
- 新增一个需要读写车辆属性的开关时,从零开始的数据链路。
阅读建议:第一遍顺着读,第二遍对照源码读。每节末尾「📌 新手记住」是高频被问到的考点。
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:43 | SettingsCarPropertyManager 内部的下层封装,直接持有 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:48 | CCP 静态配置字(车型、硬件开关)查询 + 缓存 |
…/miauto/vehicle/MiCarPropertyIds.java | 所有车信号 propId 常量(dcd/xcd 两套 framework 通过这个文件的常量交叉编译) |
…/miauto/VehiclePermanentListener.java | App 启动时常驻的信号监听器,注册一批「全局关注信号」(驾驶模式、车主模式等) |
📌 新手记住:业务 Controller 永远不会直接
new Car、不会直接getCarManager(PROPERTY_SERVICE)。一切都要经过CarPropertyMgrPreferenceController→SettingsCarPropertyManager→VehicleControlManager→CarPropertyManager。这条链上任何一环都做了缓存、超时、异常兜底,绕过它就等于裸跑。
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 上线,避免轮询;mLifecycleListener(LocalCarManager.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.init(GlobalCarPropertyManager.kt:23)new 了一个 SettingsCarPropertyManager(context),后者的构造链路 SettingsCarPropertyManager → VehicleControlManager → NewBaseCarServiceManager(NewBaseCarServiceManager.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_SERVICE(VehicleControlManager.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。正确的姿势是在 onCreateInternal 里 new SettingsCarPropertyManager(...)(基类已经做了),然后在 onPropertyManagerPrepared 回调里再去注册关心的 propId。
3. 配置字 CCP:CarConfigManager
3.1 什么是 CCP
CCP(Car Configuration Parameter)是出厂烧录到车端的硬件配置字,车辆整个生命周期不变。例如「这辆车是纯电还是增程」「有没有电动踏板」「充电口是国标还是欧标」「有没有调光天幕」「销往国内还是欧洲」。在小米座舱里,它们以 CarProperty 的形式暴露出来,propId 集中定义在 MiCarPropertyIds.CarConfig.*。
CarConfigManager(CarConfigManager.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)。它做了五件事:
- 持有
SettingsCarPropertyManager:在onCreateInternal里new出来并绑到 Fragment 生命周期(CarPropertyMgrPreferenceController.java:513)。 - 声明关心的 propId 集合:抽象方法
getPropertyIdSet()由子类实现。 - 实现
CarPropertyManagerCallback:onPropertyManagerPrepared时去注册监听、onPropertyChanged时分发给子类的onHandlePropertyChange。 - 统一封装 set 信号:
setVehicleProperty(propId, value, areaId, isSync)(CarPropertyMgrPreferenceController.java:620),子类不需要管Int/Float/IntArray/List<CarPropertyValue>的分支判断。 - 管理超时 / 错误回退:信号下发后启动一个超时计时器,超时则
restorePropertyToPrevious(CarPropertyMgrPreferenceController.java:393)把 UI 状态回滚到缓存值。
4.3 三种业务基类
| 基类 | 适用场景 | 关键抽象方法 |
|---|---|---|
BaseVehicleNewSwitchPrefController(BaseVehicleNewSwitchPrefController.kt:18) | 「开关」(如离开锁车、电动踏板、自动落锁) | getPropertyIdSet()、isPropertyOpen(propVal)、needSecondConfirm() |
BaseVehicleTabPreferenceController(BaseVehicleTabPreferenceController.kt:34) | 「文字 Tab」(如蠕行速度档位、能量模式、转向手感) | buildTabData()、propValToTabIndex()、getPropValueByTab() |
BaseVehicleImageTabPreferenceController(BaseVehicleImageTabPreferenceController.kt:27) | 「图标 Tab」(如驱动形式选择、单位选择) | buildTabData()、propValToTabIndex()、setCarProperty(tab) |
BaseVehicleImageTabPreferenceController与BaseVehicleTabPreferenceController是并行的两个抽象,都直接继承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 行代码,剩下的全是基类做的:
- 用户拨动 →
MiCarNewSwitchPreference触发handlePreferenceChanged(preference, newValue); BaseVehicleNewSwitchPrefController.handlePreferenceChanged(BaseVehicleNewSwitchPrefController.kt:128)判断是否需要二次确认,不需要则调setCarProperty(isChecked);setCarProperty(BaseVehicleNewSwitchPrefController.kt:189)调setVehicleProperty(obtainSetPropertyId(), getPropertyVal(isChecked), areaId);CarPropertyMgrPreferenceController.setVehicleProperty(CarPropertyMgrPreferenceController.java:620)调mCarPropertyManager.setCarProperty(...);SettingsCarPropertyManager.setCarProperty(SettingsCarPropertyManager.kt:423)按propVal类型(Int/Float/Array/List)分发,启动超时计时器;VehicleControlManager.setIntProperty走ThreadPoolUtils.ASYNC.execute(...)(VehicleControlManager.java:115),把setIntProperty异步发到 AOSPCarPropertyManager。
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 的 callback | SettingsCarPropertyManager.kt:662 SettingsCarPropertyEventCallback.onChangeEvent |
| 基类收到并做过滤 | CarPropertyMgrPreferenceController.java:346 onPropertyChanged |
| 子类刷新 UI | BaseVehicleNewSwitchPrefController.kt:48 onHandlePropertyChange |
4.6 几个值得记住的细节
- 同 propId 共享注册:
CarPropertyCacheManager是个单例,记录「propId → callback 集合」。即使 10 个 Controller 都关心同一个 propId,对底层CarPropertyManager.registerCallback也只调一次(CarPropertyCacheManager.java:192)。这避免了多 Controller 监听同一信号时的 N 次 Binder 注册。 - areaId 过滤:很多信号分区域(左前、右前车门等)。
BaseVehicleNewSwitchPrefController.onHandlePropertyChange第一步就是检查areaId(BaseVehicleNewSwitchPrefController.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-380、CreepSpeedPreferenceController.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/dcddif、src/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 这带来的约束
- 不要在源码里写魔法数字,必须用
mi.car.config.*提供的符号常量(如Driving.PropulsionTypeConfig.EXTENDED_RANGE_ELECTRIC),否则一套源码跑不通两套平台。 - 不要在 settingsVehicleLib 里写
if (flavor == dcd)这种判断——这一层对平台无感。真正有平台差异的业务,写到 app 模块的src/dcddif/java、src/xcddif/java(这两个目录在 app 模块下是真实存在的)。 - 新增 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/dcddif、src/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(CarPowerManager、CarCabinManager、CarDiagnosticManager 等),项目里也有零散使用(如 CarHudControlManager.kt:63、CarUserPositionManager.kt:36、CarUxRestrictionsHelper.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-136、dependencies.gradle:18-29、constants.gradle:30-46 |
| App 启动初始化 | app/src/main/java/com/android/car/settings/miauto/MainThreadStartTask.java:62 |