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):LockBeepControllerBaseLockBeepControllerBaseVehicleNewSwitchPrefController,共性逻辑抽到 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,已扩散到 micarDrivingSettingsWetModeRecommendNotifyController.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 配置映射
  • ⚠️ 多区域没过滤 areaIdonHandlePropertyChange 第一步检查 areaId
  • ⚠️ 刷新逻辑写错位置:车辆 Controller 的刷新在 onHandlePropertyChange,不是 updateState
  • ⚠️ 首次缓存抖动VehiclePropertyConfig.getPropValueForFirstCache 兜底,前提是首次缓存前有 getProperty

9. 无车自检步骤

开发时通常没车,验证链路靠日志:

  1. 编译通过:dcd / xcd 两个 flavor 都能编译(确认两套 jar 都有该 propId 常量)
  2. Controller 被实例化:logcat 搜 Controller 类名,确认 onCreateInternal 执行
  3. CarService 连接:搜 onPropertyManagerPrepared,确认 ready
  4. 注册成功:搜 CarPropertyCacheManager,确认 propId 注册
  5. 下发链路:搜 VehicleControlManager,确认 set 打印
  6. 上报链路:搜 SettingsCarPropertyEventCallback,确认回弹到达
  7. 用 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

② 确认 propIdMiCarPropertyIds.SeatAdjustControl.SEAT_HEAT_AUTO_OFF(已存在)

③ ControllerSeatHeatAutoOffController.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)
    }
}

④ XMLseat_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;
}

完成。基类处理其余一切。