09 - 新增信号功能开发模板
本篇是开发者最常用的一篇:要新增一个车控设置项(开关/Tab/进度条/卡片),照着这里抄。
每个模板都给出最小可运行代码,来自项目实际文件。
0. 先决策:用哪种模式
你的功能是什么?
│
├─ 单信号控制一个开关 ──────────► 模板 A(BaseVehicleNewSwitchPrefController)
│
├─ 单信号多挡位(低/中/高)─────► 模板 B(BaseVehicleTabPreferenceController)
│
├─ 信号值映射到图标挡位 ────────► 模板 B'(BaseVehicleImageTabPreferenceController)
│
├─ 信号驱动 SeekBar 进度 ───────► BaseVehicleProgressPrefController(参考氛围灯亮度)
│
├─ 多信号组合判定 UI 状态 ──────► 模板 C(Flow:CarPropertyRepo + UseCase + ViewModel)
│
└─ 一个页面整体联动(车模图)──► 模板 D(FragmentVehicleController)
1. 共性前置步骤(所有模板都要做)
1.1 确认配置字(功能有无)
在 base/settingsVehicleLib/.../miauto/CarConfigManager.kt 加查询方法:
fun isXxxEnable(): Boolean {
return getIntProperty(MiCarPropertyIds.CarConfig.XXX_CONFIG) != Int.MIN_VALUE
// 注意:用 Int.MIN_VALUE 判断"服务不可用",不要用 0 或 -1
}如果该配置字是动态的(运行时会变),必须
skipCache = true。参考CarConfigManager.kt:619 isCarModeInShowRoom、:843 getOwnerModeActiveState。
1.2 确认 propId 常量存在
- 在
base/settingsBaseLib/.../vehicle/MiCarPropertyIds.java找对应内部类的常量 - 如果没有:① 升级
com.mi.car.config:api(联系 SOA 团队),② 在MiCarPropertyIds.java加一行:public static final int XXX = mi.car.config.YourDomain.YOUR_SIGNAL; - 确认 dcd / xcd 两套 jar 都有该常量
1.3 找到信号的依赖关系
从 SOA MS11 表查该信号是否需要额外的”使能信号”和”异常信号”(见 01 篇 §4.2)。如果需要监听异常信号作 enable/loading,用 getObservedPropertyIdSet() + IVehiclePerformFeature。
2. 模板 A:开关(最常见,~26 行)
继承:BaseVehicleNewSwitchPrefController
范例:settingsPage/micarLockSettings/.../lock/WalkAwayLockController.kt(全 27 行)
// AI(claude) auto generated at 2026-07-12
// 类名: WalkAwayLockController
// 用途: 离车自动锁车开关
// 作用: 监听 WALK_AWAY_LOCK 信号,用户拨开关时下发指令
class WalkAwayLockController(
context: Context,
preferenceKey: String,
fragmentController: FragmentController,
uxRestrictions: CarUxRestrictions
) : BaseVehicleNewSwitchPrefController(
context, preferenceKey, fragmentController, uxRestrictions
) {
// 唯一必填:声明订阅的信号
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
add(MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK)
}
}基类已自动处理:注册 callback、缓存、超时、回滚、UI 刷新(setChecked)、二次确认。
按需 override:
| 方法 | 场景 |
|---|---|
isPropertyOpen(propVal: Int): Boolean | 枚举型开关(值≠0/1),判断”开” |
getPropertyVal(isChecked: Boolean): Int | 枚举型开关,把布尔转下发值 |
getAreaId(): Int | 多区域(左/右座椅) |
needSecondConfirm(): Boolean | 需要二次确认弹窗 |
needIntermediateState(): Boolean | 有中间态(下发后置灰等回弹) |
isValidPropVal(propVal): Boolean | 保留值不刷新 |
枚举型开关范例(抽两层 Base):LockBeepController → BaseLockBeepController → BaseVehicleNewSwitchPrefController,共性逻辑抽到 BaseLockBeepController。
3. 模板 B:文字 Tab(多挡位)
继承:BaseVehicleTabPreferenceController
范例:settingsPage/VehicleBodyControl/.../service/WiperRainSensorTabLayPrefController.kt(全 61 行)
class WiperRainSensorTabLayPrefController(
context: Context, preferenceKey: String,
fragmentController: FragmentController, uxRestrictions: CarUxRestrictions
) : BaseVehicleTabPreferenceController(
context, preferenceKey, fragmentController, uxRestrictions
) {
// 1. 订阅信号
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
add(MiCarPropertyIds.Service.WIPER_RAIN_SENSOR_DETECT_LEVEL)
}
// 2. 信号值 → tab 索引(自动驱动 UI 选中态)
override fun propValToTabIndex(propertyValToIndex: Int): Int = when (propertyValToIndex) {
RAIN_SENSOR_LEVEL_LOW -> 0
RAIN_SENSOR_LEVEL_NORMAL -> 1
RAIN_SENSOR_LEVEL_FAST -> 2
RAIN_SENSOR_LEVEL_FASTEST -> 3
else -> -1
}
// 3. Tab 元信息(文案 + 对应值);点击时基类拿 tab.data 调 setVehicleProperty
override fun buildTabData(): Array<CarUiTabLayout.Tab<*>> = arrayOf(
CarUiTabLayout.Tab<Any>(context.getString(R.string.xxx_low)).setData(RAIN_SENSOR_LEVEL_LOW),
CarUiTabLayout.Tab<Any>(context.getString(R.string.xxx_normal)).setData(RAIN_SENSOR_LEVEL_NORMAL),
// ...
)
}⚠️
BaseVehicleImageTabPreferenceController(图标 Tab)与BaseVehicleTabPreferenceController(文字 Tab)是兄弟关系,都直接继承CarPropertyMgrPreferenceController,不是父子。图标 Tab 需额外实现propValToTabIndex+setCarProperty(Tab<*>)+getTabPropId()。
4. 模板 C:Flow 模式(多信号组合)
适用:一次订阅多个信号、组合判定 UI 状态。参考充电模块。
4.1 UseCase:信号 → UI 状态 Flow
class XxxUseCase {
// 多信号 combine
val mXxxState by lazy {
CarPropertyRepo.mapPropFlow(
MiCarPropertyIds.XxxCtrl.MAIN_SIG to DEFAULT_AREA_ID,
MiCarPropertyIds.XxxCtrl.DEPEND_SIG to DEFAULT_AREA_ID
) { props ->
val main = props[0].intValue()
val depend = props[1].intValue()
// 组合判定为 UI 状态
XxxUiState(main, depend)
}
}
}4.2 ViewModel:暴露 StateFlow
class XxxViewModel : ViewModel() {
companion object {
fun getInstance(fragment: Fragment) =
ViewModelProvider(fragment).get(XxxViewModel::class.java)
}
private val useCase = XxxUseCase()
val mXxxState by lazy {
useCase.mXxxState.stateIn(viewModelScope, SHARING_STARTED, XxxUiState.Default)
}
}4.3 Controller:订阅 StateFlow 刷 UI
class XxxController(...) : CarPropertyMgrPreferenceController<...>(...) {
private val mViewModel by lazy {
XxxViewModel.getInstance(getFragmentController().hostFragment)
}
override fun onViewCreatedInternal() {
super.onViewCreatedInternal()
mViewModel.mXxxState.collectIn(
fragmentController.hostFragment.viewLifecycleOwner
) { state ->
preference.setSummary(state.text) // 刷 UI
}
}
}Flow 模式目前主要在
micarChargeSettings,已扩散到micarDrivingSettings(WetModeRecommendNotifyController.kt)。详见 04 篇。
5. 模板 D:Fragment 级集中订阅
适用:一个页面整体联动(如灯光右侧车模图)。
继承:FragmentVehicleController(或 BaseCarPropertyController)
范例:settingsPage/micarLightSettings/.../LightsRightFragmentController.kt
class LightsRightFragmentController(...) : FragmentVehicleController(...) {
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
// 集中订阅一束灯光状态信号
add(MiCarPropertyIds.LightCtrl.POSITION_BEAM_STATUS)
add(MiCarPropertyIds.LightCtrl.HIGH_BEAM_STATUS)
add(MiCarPropertyIds.LightCtrl.MOOD_LIGHT_TRIGGER)
// ...
}
override fun onPropertyChanged(value: CarPropertyValue<*>) {
// 根据 propId 切换车模图片
when (value.propertyId) {
MiCarPropertyIds.LightCtrl.HIGH_BEAM_STATUS -> updateHighBeamImage(value)
// ...
}
}
}6. XML 配置(所有 Preference 模式都要)
在 settingsPage/<module>/src/main/res/xml/xxx_fragment.xml:
<PreferenceCategory android:title="@string/xxx">
<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
android:key="@string/pref_key_walk_away_lock"
android:title="@string/walk_away_lock_title"
settings:controller="com.android.car.settings.miauto.lock.WalkAwayLockController"/>
</PreferenceCategory>settings:controller写全限定类名,BaseXmlParserSettingsFragment.onAttach会反射实例化(03 篇 §3.4)- 构造函数必须四参数:
(Context, String, FragmentController, CarUxRestrictions)(反射强制约定)
7. 显隐控制(按车型/配置字)
在 Fragment 重写:
@Override
public List<Integer> getPreferenceKeyResIdsToRemove() {
List<Integer> keys = new ArrayList<>();
if (!CarConfigManager.INSTANCE.isXxxEnable()) {
keys.add(R.string.pref_key_xxx); // 该车型不支持,移除整个 Preference
}
return keys;
}Controller 里用 getAvailabilityStatus() 处理”该设备根本不该看见”的静态判断。
8. 避坑清单
8.1 不要做的事
- ❌ 在 Controller 构造函数取 Car 实例
- ❌ 自己
new Car/Car.createCar - ❌ 直接
getCarManager(PROPERTY_SERVICE) - ❌ 手写
registerCallback(基类已处理) - ❌ 直接用
VehicleControlManager(不公开) - ❌ 写魔法数字 propId(用
MiCarPropertyIds.Xxx.YYY符号常量) - ❌ 写
if (flavor == dcd)(用子类化) - ❌ 在 Fragment 直接订阅信号(下沉到 Controller / ViewModel)
- ❌ 用 DataBinding / ViewBinding(项目不用)
- ❌ Flow 订阅不绑
viewLifecycleOwner
8.2 容易踩的坑
- ⚠️ 忘了
registerPropertyCallback:propId 不在 observedSet,onPropertyChanged不回调,只打"un-excepted observe propId"警告 - ⚠️ 配置字用 0 判断:要用
Int.MIN_VALUE判断服务不可用 - ⚠️ status 判断用错:用
== STATUS_AVAILABLE,不要用!= STATUS_UNAVAILABLE(小米有 TIMEOUT(4)/TIMEOUT_UNAVAILABLE(5)) - ⚠️ 下行/上行 ID 不一致:override
obtainSetPropertyId(),并在VehiclePropertyConfig.sTimeoutSignalMapping配置映射 - ⚠️ 多区域没过滤 areaId:
onHandlePropertyChange第一步检查areaId - ⚠️ 刷新逻辑写错位置:车辆 Controller 的刷新在
onHandlePropertyChange,不是updateState - ⚠️ 首次缓存抖动:
VehiclePropertyConfig.getPropValueForFirstCache兜底,前提是首次缓存前有getProperty
9. 无车自检步骤
开发时通常没车,验证链路靠日志:
- 编译通过:dcd / xcd 两个 flavor 都能编译(确认两套 jar 都有该 propId 常量)
- Controller 被实例化:logcat 搜 Controller 类名,确认
onCreateInternal执行 - CarService 连接:搜
onPropertyManagerPrepared,确认 ready - 注册成功:搜
CarPropertyCacheManager,确认 propId 注册 - 下发链路:搜
VehicleControlManager,确认 set 打印 - 上报链路:搜
SettingsCarPropertyEventCallback,确认回弹到达 - 用 mock 注入信号(见 11 篇):
adb shell dumpsys android.hardware.automotive.vehicle.IVehicle/micar --mock_from_car <PROP> -i <VAL> -a <AREA> --stat 0
10. 完整最小范例(端到端)
新增”座椅加热自动关闭”开关的完整改动:
① 配置字(CarConfigManager.kt):
fun isSeatHeatAutoOffEnable(): Boolean =
getIntProperty(MiCarPropertyIds.CarConfig.SEAT_HEAT_AUTO_OFF_CONFIG) != Int.MIN_VALUE② 确认 propId:MiCarPropertyIds.SeatAdjustControl.SEAT_HEAT_AUTO_OFF(已存在)
③ Controller(SeatHeatAutoOffController.kt):
class SeatHeatAutoOffController(
context: Context, preferenceKey: String,
fragmentController: FragmentController, uxRestrictions: CarUxRestrictions
) : BaseVehicleNewSwitchPrefController(
context, preferenceKey, fragmentController, uxRestrictions
) {
override fun getPropertyIdSet(): ArraySet<Int> = ArraySet<Int>().apply {
add(MiCarPropertyIds.SeatAdjustControl.SEAT_HEAT_AUTO_OFF)
}
}④ XML(seat_fragment.xml):
<MiCarNewSwitchPreference
android:key="@string/pref_key_seat_heat_auto_off"
android:title="@string/seat_heat_auto_off_title"
settings:controller="com.android.car.settings.miauto.seat.SeatHeatAutoOffController"/>⑤ 显隐(SeatSettingsFragment.java):
@Override
public List<Integer> getPreferenceKeyResIdsToRemove() {
List<Integer> keys = new ArrayList<>();
if (!CarConfigManager.INSTANCE.isSeatHeatAutoOffEnable()) {
keys.add(R.string.pref_key_seat_heat_auto_off);
}
return keys;
}完成。基类处理其余一切。