06 - 典型页面全链路剖析:锁车控制页
本篇概览
前面几篇我们已经分别讲过「PreferenceController 范式」「路由框架」这些机制。这一篇换一种讲法——挑一个真实页面,把所有机制串成一条线,从用户点进入页面,到改一个车控开关,再到 UI 回流刷新,全链路走一遍。
选定页面:
settingsPage/micarLockSettings模块下的「门窗锁控制页」(Fragment:VehicleLockSettingsFragment)。为什么选它:
- 它是车控类设置的「母版页」——一个 Fragment 上挂着二十多个 Controller,几乎涵盖了所有常见形态(开关型、Tab 型、跳转型、Slider 型)。看懂这一个页,其它车控页(灯光、驾驶、车身)都是同一套模板的复用。
- 它的开关型 Controller(如
WalkAwayLockController)极简,所有车控读/写/监听/可用性/回流逻辑都收敛在基类BaseVehicleNewSwitchPrefController里,读一个基类就把车控页 80% 的套路学完。- 它同时呈现了「枚举型开关」(
LockBeepController把布尔开关映射成多态枚举车控值),可以对比理解「车控值 ≠ 布尔」的场景。
读完你能掌握什么
- 看懂「首页 → 路由 → Activity → Fragment → XML → Controller → 车控接口 → 车控服务 → 回调 UI」这整条链路上每一段的真实代码位置和职责
- 给锁车页新增一个「读车控属性 / 改车控属性 / 监听变化」的开关项,能立刻知道要在哪几个文件里加什么
- 看到任何一个
xxxPrefController,能从它继承的基类一眼判断它的行为模式 - 理解
AvailabilityStatus、onHandlePropertyChange、handlePreferenceChanged、setVehicleProperty这几个车控开发最核心的扩展点
1. 入口:用户怎么进到锁车页
锁车页一共有三种典型入口,最终都汇聚到同一个 Fragment 实例。
1.1 路由路径:一个常量 + 一个注解
锁车页的路由 URI 是一个字符串常量,定义在:
base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/CarSettingsJump.kt:78-81
// 门窗锁页跳转相关
object VehicleLockControlSettings {
const val VEHICLE_LOCK_CONTROL_URI_PATH =
"$SETTING_SUBPAGE_URI_PREFIX$PAGE_VEHICLE_LOCK"
}其中前缀 SETTING_SUBPAGE_URI_PREFIX 是 "micarsettings://homepage/?subPage="(同文件第 17 行),PAGE_VEHICLE_LOCK 是页面别名常量。最终拼出来的 URI 形如:
micarsettings://homepage/?subPage=vehicle_lock
这个 URI 就是锁车页的「身份证」。
1.2 三种入口殊途同归
flowchart LR A1["首页一级菜单<br/>点击「门窗锁」"] --> R["Router 路由表<br/>匹配 VEHICLE_LOCK_CONTROL_URI_PATH"] A2["语音/外部 App<br/>发送 deeplink Intent"] --> A2b["DeeplinkActivity<br/>(透明中转)"] --> R A3["语音搜索<br/>'打开离车自动锁车'"] --> A3b["VoiceSearchManager<br/>构造 extras"] --> R R --> F["VehicleLockSettingsFragment<br/>@RouterProvider(path=...)"] F --> H["handleJump(uri)<br/>处理滚动到指定项"]
- 首页菜单点击:
BaseCarSettingsActivity.launchFragment(fragment)直接把VehicleLockSettingsFragment实例压栈,详见app/.../BaseCarSettingsActivity.java:367-380。 - 外部 URI / 语音:进入
app/.../miauto/common/DeeplinkActivity.kt——一个alpha=0的透明中转 Activity,它把 Intent extras 转交给HomepageActivity,再由 Router 分发。DeeplinkActivity关键代码在第 28-43 行,onResume延迟 20ms 后handleVoiceSearchExtra(),最终startActivity(HomepageActivity intent)。 - 语音直达具体项:通过
VoiceSearchManager.createVoiceSearchExtras构造带widgetKey的 extras,仍走HomepageActivity入口。
1.3 路由命中:Fragment 上的 @RouterProvider
VehicleLockSettingsFragment 类头上的三个注解决定了它的路由身份:
settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/vehicle/VehicleLockSettingsFragment.java:55-59
@MonitorFragment // 数据上报埋点
@VoiceSearchProvider // 允许被语音搜索框架解析
@Module(SettingsConstant.TAG_LOCK) // 归属「锁车」业务模块
@RouterProvider(path = VEHICLE_LOCK_CONTROL_URI_PATH) // ← 路由表登记
public class VehicleLockSettingsFragment extends TopLevelSettingsFragment
implements IPageRouteHandler {
...
}@RouterProvider 由 routerApt 在编译期扫描,生成 RouterHelper / RouterTabs(路径 → 类名映射表)。运行期 Router 单例启动时通过反射调用 RouterHelper.merge() 把所有路由项合并进内存,详见 settingsCommon/plugin/router/routerManager/.../Router.java:46-62:
private Router() {
Class clazz = Class.forName(ROUTER_HELPER_PATH); // 编译期生成的 RouterHelper
Method method = clazz.getDeclaredMethod("merge");
method.invoke(clazz); // 合并路由表
Iterator<String> iterator = RouterTabs.tabs().values().iterator();
while (iterator.hasNext()) {
mControllerNameHash.add(iterator.next().hashCode());
}
}Fragment onResume 时,Router.FragmentCallback.onFragmentResumed 会拿当前 URI 去 RouterTabs.tabs() 里匹配,命中后调用 IPageRouteHandler.handleJump(uri),见 Router.java:194-254。锁车页的 handleJump 实现很简单——把 URI 里的滚动锚点交给 handleScrollUri,让页面滚到指定项:
VehicleLockSettingsFragment.java:241-249
@Override
public void handleJump(Uri data) {
MLog.d("handleJump uri = " + data);
if (data == null) return;
handleScrollUri(data); // 支持「语音直达某一项并高亮滚动」
}📌 新手记住:在锁车页加新功能时,永远不需要改路由层。路由只负责「把外部 URI 变成 Fragment 实例 + 一个 handleJump 回调」,至于跳转后页面要做什么,是 Fragment 自己决定的。
2. Activity 层:谁承载 Fragment
锁车页所在 Activity 是 HomepageActivity(首页 Activity,多 Fragment 模式)。它继承自:
app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java
关键流程在 onCreate(第 149-194 行):
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.car_setting_activity);
// ...
if (mHasNewIntent) {
Fragment initFragment = getInitialFragment(); // 子类决定首个 Fragment
launchIfDifferent(initFragment); // 把它压入 fragment_container
// ...
}
}launchFragment 会根据当前是「单窗格 / 多窗格」决定是新开 Activity 还是替换 container(第 367-380 行):
@Override
public void launchFragment(Fragment fragment) {
if (mIsSinglePane) {
Intent intent = SubSettingsActivity.newInstance(this, fragment);
startActivity(intent); // 独立 Activity 承载
} else {
launchIfDifferent(fragment); // 多窗格下原地替换
}
}如果走独立 Activity,承载的是 SubSettingsActivity——它本身几乎没逻辑,只是把 Intent 里携带的 Fragment 类名反序列化出来(SubSettingsActivity.java:49-61):
@Override
protected Fragment getInitialFragment() {
String fragmentClass = getIntent().getStringExtra(KEY_SUB_SETTINGS_FRAGMENT);
Bundle fragmentArgs = getIntent().getBundleExtra(KEY_SUB_SETTINGS_FRAGMENT_ARGS);
Fragment fragment = getSupportFragmentManager().getFragmentFactory()
.instantiate(getClassLoader(), fragmentClass);
fragment.setArguments(fragmentArgs);
return fragment;
}📌 新手记住:车机大屏上锁车页一般走多窗格模式(
mIsSinglePane=false),Fragment 被replace到R.id.fragment_container里。SubSettingsActivity只在小屏 / 独立窗口场景出现。两条路最终都是「一个 Fragment 实例被压进布局」,所以业务代码无感。
3. Fragment 层:VehicleLockSettingsFragment
锁车页 Fragment 的核心代码非常简短,因为它把「显示什么、隐藏什么」拆给了 XML,把「怎么读写车控」拆给了 Controller。
VehicleLockSettingsFragment.java:59-80 是整个类最关键的几行:
public class VehicleLockSettingsFragment extends TopLevelSettingsFragment
implements IPageRouteHandler {
private BaseRightFragment mRightFragment;
@Override
public String getPageAlias() {
return PageAlias.PAGE_VEHICLE_LOCK; // 页面别名,用于埋点/路由识别
}
@Override
@XmlRes
public int getPreferenceScreenResId() {
return R.xml.miauto_vehicle_lock_ctrl_settings_fragment; // ← 唯一的 XML 真相
}
@Override
public BaseRightFragment getRightFragment() {
if (mRightFragment == null) {
mRightFragment = new DoorAndWindowLockRightFragment(); // 右侧车模/可视化
}
return mRightFragment;
}
// ...
}三件事:
- 页面别名
PAGE_VEHICLE_LOCK——埋点、首页高亮、锚点跳转都靠它。 getPreferenceScreenResId()返回R.xml.miauto_vehicle_lock_ctrl_settings_fragment,告诉父类「我这个页面长这样」。getRightFragment()返回右侧的DoorAndWindowLockRightFragment——这是车控页常见的「左侧菜单 + 右侧车模示意图」布局,右侧负责显示车门/车窗状态的车模图(详见DoorAndWindowLockRightFragment.kt,本篇不展开)。
Fragment 是怎么被「装好」的
TopLevelSettingsFragment → SettingsFragment → BaseXmlParserSettingsFragment,真正的装配逻辑在 BaseXmlParserSettingsFragment.onAttach(base/settingsBaseUi/.../common/BaseXmlParserSettingsFragment.java:274-334):
@Override
public void onAttach(Context context) {
super.onAttach(context);
// ... 校验 Activity 实现 UxRestrictionsProvider / FragmentHost ...
int xmlResId = getPreferenceScreenResId();
// 一次性解析 XML,提取 key / controller / dependKey / hiddenFeatures
List<Bundle> prefMetadataList = PreferenceXmlParser.extractMetadata(...);
// 用反射把 XML 里声明的 controller 类名实例化
mPreferenceControllers.addAll(
PreferenceControllerListHelper.getPreferenceControllersFromMetadata(
styledContext, prefMetadataList, /* fragmentController= */ this,
mUxRestrictions));
// 把每个 controller 注册成 Lifecycle 观察者,后续 onCreate/onStart 会自动回调
mPreferenceControllers.forEach(controller -> {
lifecycle.addObserver(controller);
// ...
});
}📌 新手记住:Fragment 类本身几乎不写业务逻辑。它做三件事就够:① 声明 XML、② 声明页面别名、③ 决定右侧 Fragment。剩下的事,XML 决定显示什么、Controller 决定怎么干。
4. XML 层:声明式拼装页面
XML 文件是锁车页的「设计稿」。打开 settingsPage/micarLockSettings/src/main/res/xml/miauto_vehicle_lock_ctrl_settings_fragment.xml,你会看到一个 PreferenceScreen,里面挂着一堆 MiCarPreferenceCategory(分组),每个分组下挂着各种自定义 Preference,每个 Preference 通过 settings:controller="全类名" 把行为委托给一个 Controller。
挑两段关键片段看:
<!-- 4.1 童锁 & 车窗锁,开关型,挂在 ButtonTogglePreference 上 -->
<com.android.car.settings.miauto.preferences.ButtonTogglePreference
android:key="@string/pk_settings_vehicle_quick_ctrl_locks"
android:layout="@layout/micar_ui_pref_button_layout"
settings:debounceEnable="true"
settings:controller="com.android.car.settings.miauto.vehicle.VehicleLockToggleCtrlPreferenceController" />
<!-- 4.2 离车自动锁车,布尔开关,最典型形态 -->
<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
android:key="@string/pk_settings_lock_walk_away_lock"
android:title="@string/settings_lock_walk_away_lock"
android:summary="@string/settings_lock_walk_away_lock_summary"
settings:controller="com.android.car.settings.miauto.lock.WalkAwayLockController" />
<!-- 4.3 锁车时鸣笛,本质是「枚举型」开关:LIGHT / LIGHT_AND_HORN / LIGHTS_AND_TUNE -->
<com.android.car.settings.miauto.preferences.MiCarNewSwitchPreference
android:key="@string/pk_settings_lock_lock_beep"
android:title="@string/settings_lock_lock_beep"
settings:controller="com.android.car.settings.miauto.lock.LockBeepController" />三个值得关注的属性:
| 属性 | 作用 | 示例 |
|---|---|---|
android:key | Preference 唯一标识,必须与 Controller 构造参数 preferenceKey 对应(系统按 key 匹配 Controller 与 Preference) | @string/pk_settings_lock_walk_away_lock |
settings:controller | 该 Preference 由哪个 Controller 驱动,写全限定类名 | com.android.car.settings.miauto.lock.WalkAwayLockController |
settings:dependKey | 依赖的另一个 Preference 的 key,用于「父开关关闭则子项隐藏」 | 电动车门模式控制着「车门开启角度」的显隐 |
Controller 是怎么从字符串变成对象的? 在 PreferenceControllerListHelper.createInstance(base/settingsBaseUi/.../common/PreferenceControllerListHelper.java:110-129):
private static PreferenceController createInstance(String controllerName, ...) {
Class<?> clazz = Class.forName(controllerName);
// 约定:所有 Controller 必须有这个 4 参数构造器
Constructor<?> preferenceConstructor = clazz.getConstructor(
Context.class, String.class, FragmentController.class, CarUxRestrictions.class);
Object[] params = new Object[]{context, key, fragmentController, restrictionInfo};
PreferenceController preferenceController =
(PreferenceController) preferenceConstructor.newInstance(params);
Router.getInstance().holdController(preferenceController, fragmentController.getHostFragment());
preferenceController.setDependKey(dependKey);
return preferenceController;
}这段反射代码揭示了一个强制约定:锁车页里所有 Controller(包括你将来新加的),都必须有一个 (Context, String, FragmentController, CarUxRestrictions) 构造器。回头去看 WalkAwayLockController.kt:10-15,它的构造器签名正是这四个参数。
4.4 一个页面二十多个 Controller,怎么知道哪些该显示
锁车页 XML 里挂了二十多个 Preference,但车型不同,支持的配置也不同——增程车才有「解锁加油口」,Kunlun 车型才有「车顶帐篷」。Fragment 用一个非常直白的方式做过滤:返回「要移除的 key 列表」,父类会把这些 key 对应的 Preference 从屏幕上删掉。
VehicleLockSettingsFragment.java:83-238,摘一段:
@Override
public List<Integer> getPreferenceKeyResIdsToRemove() {
List<Integer> ids = new ArrayList<>();
// 非增程车型:隐藏加油口
if (CarConfigManager.INSTANCE.getPropulsionTypeConfig()
!= Driving.PropulsionTypeConfig.EXTENDED_RANGE_ELECTRIC) {
ids.add(R.string.pk_settings_fuel_lid);
}
// 海外版:隐藏自定义锁车音效(无 toybox)
if (DeviceUtil.isOverseasRegion()) {
ids.add(R.string.pk_settings_lock_lock_beep_custom);
}
// 不支持电动门:隐藏整个「电动门」分组
if (!MiCarSettings.isElectricDoorSupported()) {
ids.add(R.string.pk_settings_quickctrl_door_category);
}
// ... 几十个配置字 / 车型判断 ...
return ids;
}📌 新手记住:在锁车页加新功能,三步:① XML 里加一个 Preference +
settings:controller="全类名";② 实现这个 Controller 类(参考下一节);③ 如果某些车型不该显示,在getPreferenceKeyResIdsToRemove()里加一个判断,把 key 扔进列表。
5. Controller 层:锁车页的「行为大脑」
这是这一篇最重要的一节。锁车页有二十多个 Controller,但它们的行为模式其实只有少数几种。先看懂最简单的 WalkAwayLockController,再看枚举型 LockBeepController,就能举一反三。
5.1 类继承关系(先看全局)
classDiagram direction LR class PreferenceController~V~ { <<abstract>> +getAvailabilityStatus() int #onCreateInternal() #handlePreferenceChanged(V, Object) boolean #refreshUi() } class CarPropertyMgrPreferenceController~V~ { <<abstract>> #SettingsCarPropertyManager mCarPropertyManager +onPropertyChanged(CarPropertyValue) +onSetPropertyTimeout(int,int) #setVehicleProperty(int,Object,int) boolean #abstract getPropertyIdSet() ArraySet } class BaseVehicleNewSwitchPrefController { <<abstract>> #onHandlePropertyChange(CarPropertyValue, boolean) #handlePreferenceChanged(MiCarNewSwitchPreference, Any) boolean #setCarProperty(boolean) #isPropertyOpen(int) boolean } class BaseLockBeepController { <<abstract>> #isValidPropVal(CarPropertyValue) boolean #isPropertyOpen(int) boolean #getPropertyVal(boolean) int } class WalkAwayLockController { +getPropertyIdSet() ArraySet } class PGearAutoLockController { +getPropertyIdSet() ArraySet } class LockBeepController { +getPropertyIdSet() ArraySet +isPropertyOpen(int) boolean } PreferenceController <|-- CarPropertyMgrPreferenceController CarPropertyMgrPreferenceController <|-- BaseVehicleNewSwitchPrefController BaseVehicleNewSwitchPrefController <|-- BaseLockBeepController BaseVehicleNewSwitchPrefController <|-- WalkAwayLockController BaseVehicleNewSwitchPrefController <|-- PGearAutoLockController BaseLockBeepController <|-- LockBeepController
记住这个层级,它就是车控类 Controller 的「家谱」:
PreferenceController:所有 Controller 的根,定义生命周期 +AvailabilityStatusCarPropertyMgrPreferenceController:所有「需要读写车控」的 Controller 的根,封装信号注册/回调/超时BaseVehicleNewSwitchPrefController:开关型 Controller 的根,封装「点击 → 下发 → 回流 → 刷新勾选」- 具体子类只负责「声明我关心哪个车控属性 ID」
5.2 WalkAwayLockController:极简的开关型
完整代码只有 26 行(settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/WalkAwayLockController.kt):
class WalkAwayLockController(
context: Context?,
preferenceKey: String?,
fragmentController: FragmentController?,
uxRestrictions: CarUxRestrictions?
) : BaseVehicleNewSwitchPrefController(
context, preferenceKey, fragmentController, uxRestrictions
) {
override fun getPropertyIdSet(): ArraySet<Int> {
val ids = ArraySet<Int>()
ids.add(MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK) // 离车自动锁车属性 ID
return ids
}
}它只做一件事:告诉基类「我关心 WALK_AWAY_LOCK 这个车控属性」。所有其它逻辑——什么时候 enable、点击怎么下发、信号回流怎么刷新 UI——全部继承自基类。
MiCarPropertyIds.LockCtrl.WALK_AWAY_LOCK 的真实值(base/settingsBaseLib/.../vehicle/MiCarPropertyIds.java:879-897):
public static final int WALK_AWAY_LOCK = Door.LEAVING_AUTO_LOCK; // 离车自动锁车
public static final int P_GEAR_AUTO_LOCK = Door.PARK_AUTO_UNLOCK; // P 挡解锁
public static final int LOCK_BEEP = Door.LOCK_INDICATION; // 锁车鸣笛
public static final int DOOR_CINCH_ENABLE_STATUS = Door.DOOR_CINCH_ENABLE_STATUS;这些常量最终对应到车控 HAL 层的 Vehicle Property ID(由车控系统团队维护)。
5.3 基类做了什么:BaseVehicleNewSwitchPrefController
这是车控开关的 80% 套路所在。 它继承自 CarPropertyMgrPreferenceController<MiCarNewSwitchPreference>,下面三个方法覆盖了「读、写、回流」全链路。
A. 创建时读当前值(onCreateInternal)
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleNewSwitchPrefController.kt:199-216
override fun onCreateInternal() {
super.onCreateInternal()
// 注册一个「开关拦截器」,用于点击前置校验(例如行车中禁止切换)
preference.setPreCheck(object : PreCheckSwitch.PreCheckInterceptor {
override fun isPrevent(targetState: Boolean): Boolean {
return preference.isEnabled && onPreCheck(targetState)
}
override fun onCallback() { onPrevent() }
})
// 用当前车控属性值初始化开关状态
obtainSetPropertyId()?.let {
preference.setChecked(isPropertyOpen(getIntProperty(it, areaId)))
}
}obtainSetPropertyId() 默认返回 getPropertyIdSet() 里第一个属性 ID(也就是子类声明的那个)。isPropertyOpen(int) 默认判断「值 == CommonParams.OnOff.ON」,所以 WalkAwayLock 这种「布尔型」车控完全不用重写。
B. 用户点击 → 下发车控(handlePreferenceChanged)
BaseVehicleNewSwitchPrefController.kt:128-149
override fun handlePreferenceChanged(
preference: MiCarNewSwitchPreference, newValue: Any
): Boolean {
if (newValue is Boolean) {
if (needSecondConfirm()) {
showConfirmDialog(newValue) // 需要二次确认的场景
return false
} else {
// 埋点上报
getTrackTipKey()?.let { ... TrackEvent.create(Event.CLICK, it).report() }
setCarProperty(newValue) // → 真正下发车控
}
return true
}
return super.handlePreferenceChanged(preference, newValue)
}setCarProperty 把布尔值映射成车控值,再调基类的 setVehicleProperty(第 189-197 行):
protected open fun setCarProperty(isChecked: Boolean) {
if (needIntermediateState()) {
preference.isEnabled = false // 切换中禁用,避免连点
}
setVehicleProperty(obtainSetPropertyId(), getPropertyVal(isChecked), areaId)
updatePreferencesVisibleWhenStatusChange(isChecked) // 联动子项显隐
}getPropertyVal(boolean) 默认 isChecked ? OnOff.ON : OnOff.OFF,对布尔型车控不用改。
C. 信号回流 → 刷新 UI(onHandlePropertyChange)
BaseVehicleNewSwitchPrefController.kt:48-68
override fun onHandlePropertyChange(
propertyValue: CarPropertyValue<*>?,
updateUIByPropVal: Boolean
) {
// 同一信号不同 area 过滤(避免一个信号触发多个开关)
if (VehiclePropertyUtil.getAreaId(propertyValue) != areaId) return
val propVal = VehiclePropertyUtil.getIntPropertyValue(propertyValue)
preference.isEnabled = VehiclePropertyUtil.isPropertyAvailable(propertyValue)
&& isInnerLogicAvailable(propertyValue)
if (updateUIByPropVal) {
preference.setChecked(isPropertyOpen(propVal)) // ← 把车控值反映到开关
mIsPreventOperationWhenEnable = isPreventSwitchSlide(propVal)
}
if (needIntermediateState()) {
preference.setLoading(isInLoadingState(propertyValue)) // 中间加载态
}
super.onHandlePropertyChange(propertyValue, updateUIByPropVal)
}📌 新手记住:写一个新开关型车控 Controller,只需要三件事:① 继承
BaseVehicleNewSwitchPrefController;② 实现getPropertyIdSet()返回关心的属性 ID;③ 如果车控值不是简单布尔,重写isPropertyOpen/getPropertyVal。仅此而已。
5.4 LockBeepController:枚举型开关的样板
「锁车时鸣笛」对应的车控属性 LOCK_BEEP 不是布尔,而是一个枚举:可以是 LIGHT(仅灯光)、LIGHT_AND_HORN(灯光+鸣笛)、LIGHTS_AND_TUNE(灯光+音效)。开关只能表达「开 / 关」,所以需要双向映射。
settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/LockBeepController.kt
class LockBeepController(...) : BaseLockBeepController(...) {
override fun getPropertyIdSet(): ArraySet<Int> {
val ids = ArraySet<Int>()
ids.add(MiCarPropertyIds.LockCtrl.LOCK_BEEP)
return ids
}
override fun isPropertyOpen(propVal: Int): Boolean {
// UI 开关勾选 ⇔ 车控值为「灯光+鸣笛」
return propVal == Door.LockIndicationMode.LIGHT_AND_HORN
}
}更通用的逻辑收敛在中间基类 BaseLockBeepController(.../lock/BaseLockBeepController.kt):
abstract class BaseLockBeepController(...) : BaseVehicleNewSwitchPrefController(...) {
// 哪些值是「合法可见」的(保留值/不支持值会让 UI 保持上一状态)
override fun isValidPropVal(vehicleProperty: CarPropertyValue<*>?): Boolean {
val unlockState = VehiclePropertyUtil.getIntPropertyValue(it)
return unlockState == Door.LockIndicationMode.LIGHT
|| unlockState == Door.LockIndicationMode.LIGHT_AND_HORN
|| unlockState == Door.LockIndicationMode.LIGHTS_AND_TUNE
}
// 开关勾选 ↔ LIGHT_AND_HORN,开关未勾选 ↔ LIGHT(只闪灯不响)
override fun isPropertyOpen(propVal: Int): Boolean {
return propVal == Door.LockIndicationMode.LIGHT_AND_HORN
}
// 用户切换开关时,把布尔翻译成车控枚举值下发
override fun getPropertyVal(isChecked: Boolean): Int {
return if (isChecked) Door.LockIndicationMode.LIGHT_AND_HORN
else Door.LockIndicationMode.LIGHT
}
}对比 WalkAwayLock,多了一层「布尔 ↔ 枚举」映射,仅此而已。
5.5 Controller 的 AvailabilityStatus 状态流转
PreferenceController 用一个 @AvailabilityStatus 注解的 int 决定 Preference 的命运(base/settingsBaseUi/.../common/PreferenceController.java:117-148):
public static final int AVAILABLE = 0; // 正常可用
public static final int CONDITIONALLY_UNAVAILABLE = 1; // 当前不可用(隐藏)
public static final int UNSUPPORTED_ON_DEVICE = 2; // 设备永远不支持(隐藏)
public static final int DISABLED_FOR_PROFILE = 3; // 当前用户不能改(隐藏)
public static final int AVAILABLE_FOR_VIEWING = 4; // 可见但不可改(置灰)锁车页的开关型 Controller 默认 AVAILABLE,但页面是否真的可见,还取决于 Fragment 的 getPreferenceKeyResIdsToRemove()(第 4.4 节);是否真的可点,还取决于信号回流时 onHandlePropertyChange 设置的 preference.isEnabled(第 5.3.C)。
stateDiagram-v2 [*] --> UNSUPPORTED_ON_DEVICE: 车型/配置字不支持\n(Fragment 把 key 加入 RemoveList) [*] --> AVAILABLE: 车型支持 UNSUPPORTED_ON_DEVICE --> [*]: onCreate 时 Preference.setVisible(false)\n生命周期方法都不回调 AVAILABLE --> DISABLED: 信号回流时\nisPropertyAvailable=false\n或 isValidPropVal=false DISABLED --> AVAILABLE: 信号恢复\n(preference.isEnabled=true) AVAILABLE --> SWITCH_ON: 车控值 == ON\n(preference.setChecked(true)) AVAILABLE --> SWITCH_OFF: 车控值 == OFF\n(preference.setChecked(false)) SWITCH_ON --> SWITCH_OFF: 用户点击 / 车控主动上报 SWITCH_OFF --> SWITCH_ON: 用户点击 / 车控主动上报 SWITCH_ON --> LOADING: 下发中(needIntermediateState)\npreference.setLoading(true) LOADING --> SWITCH_ON: 收到信号回流\nonHandlePropertyChange LOADING --> SWITCH_OFF: 超时回滚\nonSetPropertyTimeout
注意两条隐式规则:
-
UNSUPPORTED_ON_DEVICE会跳过所有生命周期回调(PreferenceController.java:401-412),所以这种状态的 Controller 不会去注册车控监听,节省开销。 -
车控类 Controller 的 enable/disable 不走原生
AVAILABLE_FOR_VIEWING,而是由onHandlePropertyChange根据isPropertyAvailable直接改preference.isEnabled。PreferenceController.refreshUi()第 369 行注释写明了这一点:// Note: 我们根据信号来决定 preference 是否 enable,故去掉此处原生逻辑 // mPreference.setEnabled(getAvailabilityStatus() != AVAILABLE_FOR_VIEWING);
📌 新手记住:写新 Controller 时不要在
getAvailabilityStatus()里写复杂的车控可用性判断——那是onHandlePropertyChange的职责。getAvailabilityStatus一般只用来处理「这个设备/这个用户根本不该看见这个项」的静态判断。
6. 车辆接口层:从 Controller 到底层 HAL
Controller 调到的最直接类是 SettingsCarPropertyManager(车控层 facade,单例封装)。
6.1 分层关系
flowchart TD subgraph 业务层 WC["WalkAwayLockController<br/>(具体子类)"] BC["BaseVehicleNewSwitchPrefController"] CM["CarPropertyMgrPreferenceController<br/>(CarPropertyManagerCallback 实现)"] end subgraph 车控Facade SC["SettingsCarPropertyManager<br/>注册/解注回调、缓存、超时监测"] end subgraph 车控底层封装 VC["VehicleControlManager<br/>extends NewBaseCarServiceManager"] LC["LocalCarManager / CarPropertyCacheManager"] end subgraph AOSP CP["CarPropertyManager<br/>(Car.PROPERTY_SERVICE)"] CH["CarService(HAL)<br/>车辆属性实际读写"] end WC --> BC BC --> CM CM -- "setVehicleProperty / getProperty / registerPropertyCallback" --> SC SC -- "setIntProperty / getProperty / registerPropertyCallback" --> VC VC --> LC LC --> CP CP -- "Binder" --> CH
6.2 真实代码位置
CarPropertyMgrPreferenceController(base/settingsVehicleLib/.../common/CarPropertyMgrPreferenceController.java)持有mCarPropertyManager: SettingsCarPropertyManager,在onCreateInternal(第 511-518 行)创建实例:mCarPropertyManager = new SettingsCarPropertyManager(getContext(), getFragmentController().getSettingsLifecycle()); mCarPropertyManager.setHolderName(getImplClassName() + "@" + Objects.hashCode(this));SettingsCarPropertyManager(base/settingsVehicleLib/.../vehicle/SettingsCarPropertyManager.kt)持有mCarPropMgr: VehicleControlManager(第 96-98 行),所有方法最终都委托过去。VehicleControlManager(base/settingsVehicleLib/.../vehicle/VehicleControlManager.java)继承NewBaseCarServiceManager<CarPropertyManager>,getServiceName()返回Car.PROPERTY_SERVICE(第 51-54 行):@Override protected String getServiceName() { return Car.PROPERTY_SERVICE; // 拿到 AOSP 的 CarPropertyManager }- 真正的读写在
VehicleControlManager第 114-127 行:public void setIntPropertyBySync(int propertyId, int propVal, int areaId) { try { mCarServiceManager.setIntProperty(propertyId, areaId, propVal); } catch (Exception e) { MLog.e(e, "Can't set %d for %s, Car service disconnect.", propVal, toHexId(propertyId)); } }
6.3 SettingsCarPropertyManager 三件关键事
它不只是个转发器,还做了三件 Controller 不关心但必须有的「脏活」:
- 信号监听注册:
registerPropertyCallback(ids)把 Controller 关心的所有属性 ID 注册到底层CarPropertyManager(第 237-255 行),底层信号一变,回调进SettingsCarPropertyEventCallback.onChangeEvent(第 662-685 行)。 - 超时监测:每次
setCarProperty会startTimeOutMonitor(第 530-562 行)开一个 Handler 延时任务。如果在窗口期内没收到回流信号,触发onSetPropertyTimeout让 Controller 回滚 UI。 - 值缓存:
mPropertiesCache按propId#areaId缓存最近一次的CarPropertyValue,避免频繁跨进程查询(第 580-609 行)。
📌 新手记住:业务代码永远不要直接
new VehicleControlManager,也永远不要直接碰 AOSP 的CarPropertyManager。统一通过SettingsCarPropertyManager走。它对外的 API 就是getProperty/setCarProperty/registerPropertyCallback三件套,外加一个CarPropertyManagerCallback回调接口(base/settingsVehicleLib/.../vehicle/CarPropertyManagerCallback.java)。
7. 数据回流:车控属性变化怎么回到 UI
现在把第 5、6 节的「写」反向走一遍——车控系统主动上报属性变化,最终是怎么让 MiCarNewSwitchPreference 的勾选状态改变的。
7.1 回调接口:CarPropertyManagerCallback
CarPropertyMgrPreferenceController 实现了这个接口(base/settingsVehicleLib/.../vehicle/CarPropertyManagerCallback.java):
public interface CarPropertyManagerCallback {
default void onPropertyManagerPrepared() {} // CarService 连接就绪
default void onPropertyChanged(CarPropertyValue<?> value) {} // 信号变化
default void onSetPropertyTimeout(int propId, int areaId) {} // 下发超时
default void onSetPropertyError(int propId, int areaId) {} // 下发错误
default CarPropertyTimeoutFlag getTimeoutFlag(CarPropertyValue<?> value) { ... }
default boolean isIgnoreCache(CarPropertyValue<?> value) { return false; }
}7.2 信号回流的完整路径
底层 CarPropertyManager 的 CarPropertyEventCallback.onChangeEvent 被触发后(SettingsCarPropertyManager.kt:662-685):
override fun onChangeEvent(carPropertyValue: CarPropertyValue<*>?) {
carPropertyValue?.let {
// ... mock 测试分支 ...
cachePropVal(it)
processTimeout(it, mCallback?.getTimeoutFlag(it)) // 取消或重启超时监测
if (mObservedPropertyIdSet.contains(it.propertyId)) {
mCallback?.onPropertyChanged(it) // → 业务层回调
}
}
}CarPropertyMgrPreferenceController.onPropertyChanged(第 346-384 行)做了几个判断(依赖信号映射、是否忽略此次回流、是否要更新 UI),最后调到 onHandlePropertyChange(propertyValue, shouldUpdateUI)——这就是第 5.3.C 节看到的方法。
7.3 三类回流场景
| 场景 | 触发 | Controller 回调 | UI 表现 |
|---|---|---|---|
| 用户自己点击开关成功 | 下发 → HAL 上报新值 | onPropertyChanged → onHandlePropertyChange | setChecked(新值) |
| 别处修改了同一属性(如车控面板) | HAL 主动上报 | 同上 | UI 自动同步 |
| 下发超时(信号没回来) | mTimeoutHandler 触发 | onSetPropertyTimeout → restorePropertyToPrevious | 开关回退到旧值 |
| 下发错误(属性不可写等) | onErrorEvent | onSetPropertyError → restorePropertyToPrevious | 开关回退到旧值 |
CarPropertyMgrPreferenceController.java:392-408 的超时/错误处理:
@Override
public void onSetPropertyTimeout(int propId, int areaId) {
restorePropertyToPrevious(propId, areaId); // 把 UI 回退到缓存里的旧值
updateChildVisibleWhenSetPropertyFail(propId, areaId);
}
@Override
public void onSetPropertyError(int propId, int areaId) {
if (shouldIgnorePropertyFeedback(propId, areaId)) {
onIgnorePropertyFeedback(propId, areaId);
return;
}
restorePropertyToPrevious(propId, areaId);
updateChildVisibleWhenSetPropertyFail(propId, areaId);
}restorePropertyToPrevious(第 125-133 行)会重新读一次缓存里的 CarPropertyValue,再喂回 onHandlePropertyChange,让 UI 回到下发前的状态。
📌 新手记住:你写 Controller 时不需要自己处理失败回滚——基类已经做了。你只需要在
onHandlePropertyChange里正确地「把车控值翻译成 UI 状态」,剩下的事基类全包了。
8. 全链路串联:一次「打开离车自动锁车」的完整旅程
把前 7 节拼起来。下面这张时序图就是这一篇的核心,建议反复看。
sequenceDiagram autonumber actor U as 用户 participant H as HomepageActivity<br/>(BaseCarSettingsActivity) participant R as Router participant F as VehicleLockSettingsFragment<br/>(IPageRouteHandler) participant XML as miauto_vehicle_lock_ctrl_...xml participant HL as PreferenceControllerListHelper participant WC as WalkAwayLockController participant BC as BaseVehicleNewSwitchPrefController participant CM as CarPropertyMgrPreferenceController participant SC as SettingsCarPropertyManager participant VC as VehicleControlManager participant CP as CarPropertyManager<br/>(AOSP) participant HAL as CarService/HAL Note over U,HAL: 阶段 A:进入锁车页 U->>H: 点击「门窗锁」入口 H->>H: onCreate(): getInitialFragment() = VehicleLockSettingsFragment H->>F: launchFragment(VehicleLockSettingsFragment) F->>F: onAttach(): 解析 XML 提取 metadata F->>HL: getPreferenceControllersFromMetadata(xml) HL->>HL: 反射创建 WalkAwayLockController(Context,key,frag,UxR) HL-->>F: 返回 20+ 个 Controller 列表 F->>F: lifecycle.addObserver(每个 Controller) Note over WC,HAL: 阶段 B:Controller 初始化( onCreate ) WC->>BC: onCreateInternal() BC->>CM: super.onCreateInternal(): new SettingsCarPropertyManager BC->>WC: getIntProperty(WALK_AWAY_LOCK) WC->>CM: getIntProperty(propId) CM->>SC: getProperty(propId) SC->>VC: getProperty(propId, areaId) VC->>CP: mCarServiceManager.getProperty(...) CP-->>VC: CarPropertyValue VC-->>SC: CarPropertyValue SC-->>CM: 缓存并返回 CM-->>BC: 当前属性值(ON/OFF) BC->>BC: preference.setChecked(isPropertyOpen(value)) BC->>CM: registerPropertyCallback({WALK_AWAY_LOCK}) CM->>SC: setCarManagerCallback(this) SC->>VC: registerCarServiceListener Note over SC: CarService 就绪后触发 onPropertyManagerPrepared<br/>→ 真正 registerPropertyCallback(ids) Note over U,HAL: 阶段 C:用户切换开关 U->>BC: 点击 MiCarNewSwitchPreference (newValue=true) BC->>BC: handlePreferenceChanged(preference, true) BC->>BC: setCarProperty(true) BC->>WC: getPropertyVal(true) → CommonParams.OnOff.ON BC->>CM: setVehicleProperty(WALK_AWAY_LOCK, ON, areaId) CM->>SC: setCarProperty(propId, ON, areaId) SC->>VC: setIntProperty(propId, ON, areaId) VC->>CP: mCarServiceManager.setIntProperty(...) CP->>HAL: Binder → CarService → 写车辆属性 SC->>SC: startTimeOutMonitor(propId, ON, areaId) Note over BC: 如果 needIntermediateState<br/>preference.isEnabled=false(中间加载态) Note over HAL,BC: 阶段 D:信号回流刷新 UI HAL-->>CP: 车控属性变化事件 CP-->>VC: onChangeEvent(CarPropertyValue) VC-->>SC: (经 CarPropertyCacheManager 转发) SC->>SC: cachePropVal + processTimeout(取消超时计时) SC->>CM: onPropertyChanged(CarPropertyValue) CM->>CM: 检查 shouldUpdateUIByPropVal / 依赖映射 CM->>BC: onHandlePropertyChange(value, true) BC->>BC: areaId 校验、isPropertyAvailable BC->>BC: preference.setChecked(isPropertyOpen(ON)) BC-->>U: 开关 UI 切换为「开启」状态 ✅ Note over U,HAL: 阶段 E(异常):下发超时 SC->>SC: mTimeoutHandler 触发 SC->>CM: onSetPropertyTimeout(propId, areaId) CM->>CM: restorePropertyToPrevious(propId, areaId) CM->>BC: onHandlePropertyChange(旧缓存值) BC->>BC: preference.setChecked(false) ⏪ 回滚
把这张图读成一句话
「点击 → Controller.handlePreferenceChanged → SettingsCarPropertyManager.setCarProperty → VehicleControlManager.setIntProperty → AOSP CarPropertyManager → Binder 进 CarService → 写车辆属性;HAL 上报 → CarPropertyEventCallback.onChangeEvent → SettingsCarPropertyManager.onPropertyChanged → Controller.onHandlePropertyChange → preference.setChecked 刷新 UI」
整条链路里,WalkAwayLockController 这个具体类只贡献了「属性 ID」这一行代码。
9. 「Fragment + N 个 Controller」结构图
最后用一张结构图收尾,让你一眼看清锁车页的组成:
flowchart TB subgraph Activity HA["HomepageActivity<br/>(BaseCarSettingsActivity)"] end subgraph "VehicleLockSettingsFragment (左侧菜单)" F["VehicleLockSettingsFragment<br/>extends TopLevelSettingsFragment<br/>implements IPageRouteHandler"] F -->|"getPreferenceScreenResId()"| XML["R.xml.miauto_vehicle_lock_ctrl_settings_fragment"] F -->|"getPreferenceKeyResIdsToRemove()"| RM["按车型/配置字移除项<br/>(燃油车隐藏加油口等)"] F -->|"handleJump(uri)"| HS["handleScrollUri<br/>(语音直达滚动)"] end subgraph "DoorAndWindowLockRightFragment (右侧车模)" RF["DoorAndWindowLockRightFragment<br/>extends BaseRightFragment"] end HA --> F HA --> RF subgraph "XML 声明的 Controller 群" XML --> C1["WalkAwayLockController<br/>(离车自动锁车·布尔)"] XML --> C2["PGearAutoLockController<br/>(P挡解锁·布尔)"] XML --> C3["LockBeepController<br/>(锁车鸣笛·枚举)"] XML --> C4["BaseLockBeepController<br/>(中间基类)"] XML --> C5["VehicleDoorToggleCtrlPreferenceController<br/>(车门·按钮型)"] XML --> C6["DoorModeLockController<br/>(车门模式·Tab型)"] XML --> C7["ElectricReleaseHeadGateController<br/>(前备箱·电释放)"] XML --> C8["HeadGateHeightPrefController<br/>(前备箱高度·Slider)"] XML --> C9["RoofPreferenceController<br/>(升降车顶)"] XML --> C10["... 共 20+ Controller"] end subgraph "公共基类" BC["BaseVehicleNewSwitchPrefController<br/>读写/回流/二次确认/中间态"] CM["CarPropertyMgrPreferenceController<br/>注册监听/回调/超时/缓存"] PC["PreferenceController<br/>生命周期/AvailabilityStatus"] BC --> CM CM --> PC end C1 --> BC C2 --> BC C3 --> C4 C4 --> BC C5 --> CM C6 --> CM C7 --> CM C8 --> CM C9 --> CM subgraph "车控 Facade" SC["SettingsCarPropertyManager<br/>统一入口"] end CM --> SC
10. 实战清单:在锁车页加一个新开关
把全篇要点收成一个可操作的 checklist:
- 明确车控属性:拿到系统车控团队给的 propertyId(如
Door.XXX_YYY),把它加到MiCarPropertyIds.LockCtrl常量类(base/settingsBaseLib/.../vehicle/MiCarPropertyIds.java)。 - 写 Controller:在
settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/下新建XxxLockController.kt:- 继承
BaseVehicleNewSwitchPrefController - 构造器签名
(Context, String, FragmentController, CarUxRestrictions)(必填) - 实现
getPropertyIdSet()返回{你的属性ID} - 如果是枚举型,重写
isPropertyOpen/getPropertyVal/isValidPropVal
- 继承
- 加 Preference:在
miauto_vehicle_lock_ctrl_settings_fragment.xml里加一个<MiCarNewSwitchPreference>,写android:key和settings:controller="全类名"。 - 配置字/车型过滤:在
VehicleLockSettingsFragment.getPreferenceKeyResIdsToRemove()里加判断,把不该显示的车型的 key 加进移除列表。 - 验证回流:手动改车控值(用车控命令工具或实车),观察开关是否同步切换;点开关,观察是否真正下发(看 logcat 中
>>> Done set propVal字样)。 - 超时/失败:故意拔掉车控连接,点开关,确认 UI 会自动回滚(基类
onSetPropertyTimeout兜底)。 - 埋点:如果需要,重写
getTrackTipKey/getTrackParamKey即可自动上报点击事件。
11. 关键文件速查表
| 角色 | 文件(相对仓库根) |
|---|---|
| Fragment | settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/vehicle/VehicleLockSettingsFragment.java |
| 页面 XML | settingsPage/micarLockSettings/src/main/res/xml/miauto_vehicle_lock_ctrl_settings_fragment.xml |
| 布尔开关 Controller 样板 | settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/WalkAwayLockController.kt |
| 枚举开关 Controller 样板 | settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/LockBeepController.kt |
| 枚举开关中间基类 | settingsPage/micarLockSettings/src/main/java/com/android/car/settings/miauto/lock/BaseLockBeepController.kt |
| 开关 Controller 根基类 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleNewSwitchPrefController.kt |
| 车控通用基类 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java |
| Controller 根基类 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceController.java |
| 车控 Facade | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/SettingsCarPropertyManager.kt |
| 车控底层封装 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehicleControlManager.java |
| 车控回调接口 | base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/CarPropertyManagerCallback.java |
| 路由 URI 常量 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/pageroute/CarSettingsJump.kt |
| 路由运行期 | settingsCommon/plugin/router/routerManager/src/main/java/com/android/car/settings/router/manager/Router.java |
| Controller 反射实例化 | base/settingsBaseUi/src/main/java/com/android/car/settings/common/PreferenceControllerListHelper.java |
| 入口 Activity | app/src/main/java/com/android/car/settings/common/BaseCarSettingsActivity.java |
| Deeplink 中转 | app/src/main/java/com/android/car/settings/miauto/common/DeeplinkActivity.kt |
| 车控属性 ID 表 | base/settingsBaseLib/src/main/java/com/android/car/settings/miauto/vehicle/MiCarPropertyIds.java |
结语:锁车页之所以复杂,是因为它项多;但每一项背后的套路其实只有「布尔开关」「枚举开关」「Tab 切换」「Slider 调节」「按钮型」这几种模板。看懂
WalkAwayLockController这 26 行代码 + 它继承链上的三个基类,你就拿走了车控类设置的「万能钥匙」。其它页面(灯光、驾驶、车身)只是属性 ID 和 XML 不同,套路完全一致。