手套箱 06 · 属性通道与台架 mock
基线声明:行号以 dev 基线工作区
/home/zbc/car/fuben4_micarsettings/MiCarSettings-5458412(origin/dev @ 930be9145,下称 W)为准;出货对照/home/zbc/car/pack_a17_cn_20260923(pack_a17_cn_20260923 快照);平台源码/home/zbc/pangu/xcd_xring_a17_fermi/vendor/micar。 mock 方法论来自共享 skillcar-signal-mock-test(KNOWLEDGE.md / LESSONS.md / references/场景-手套箱入口与密码页.md),其中入口与密码页配方已在 mach 台架(Xiaomi YU7 GT,A14,HIDL 通道)于 2026-10-19/20 实证。 本篇回答三件事:Settings 怎么读/写车辆属性、缓存与重连机制怎么工作、台架怎么把手套箱界面调出来。
0. 一张总图先立坐标
flowchart TD subgraph APP["App 调用方"] FRAG["VehicleControlSettingsFragment<br/>显隐门控 :168-172"] CTRL["GloveBoxController<br/>页面控制器"] CCMQ["CarConfigManager<br/>CCP 进程级静态缓存"] VPL["VehiclePermanentListener<br/>常驻监听单例"] end subgraph L1["L1 页面属性管理器(每控制器一个)"] SCPM["SettingsCarPropertyManager<br/>缓存+超时监测+校验"] end subgraph L0["L0 全局入口(非页面调用方)"] GCPM["GlobalCarPropertyManager<br/>SoftReference 持有一个 SCPM"] end subgraph L2["L2 全局缓存分发(进程单例)"] CPCM["CarPropertyCacheManager<br/>每 propId 只向远端注册一次"] end subgraph L3["L3 服务连接层"] VCM["VehicleControlManager<br/>extends NewBaseMiCarPropertyServiceManager"] MPM["MiCarPropertyManager<br/>(CarService binder)"] end VHAL["VHAL / vehicled(台架 mock 注入点)"] FRAG -->|① LOCK_TYPE_CONFIG 0x6140B409<br/>经 CarConfigManager 静态读| CCMQ CCMQ --> GCPM CTRL -->|② GLOVE_BOX 0x6140B407<br/>注册监听 + 点击 SET| SCPM CTRL -->|③ UNAVL_REASON 0x6140B40A<br/>按需 getIntProperty| SCPM VPL -->|④ BUTTON_STATUS 0x61401781<br/>(物理按键,常驻监听)| SCPM GCPM --> SCPM0["内部持有的 SCPM"] --> VCM SCPM --> VCM SCPM -.注册/注销回调.-> CPCM VCM --> CPCM CPCM --> MPM VCM --> MPM MPM --> VHAL
四个手套箱属性各走一条不同的路,这是理解后续所有台架现象的前提:
| 属性 | propId | 调用方 | 走的通道 | 缓存特征 |
|---|---|---|---|---|
| GLOVE_BOX_LOCK_TYPE_CONFIG | 0x6140B409 / 1631630345 | Fragment 显隐门控 → CarConfigManager.getGloveBoxLockType() | CarConfigManager → GlobalCarPropertyManager | 进程级静态缓存,mock 后必须 force-stop |
| GLOVE_BOX | 0x6140B407 / 1631630343 | GloveBoxController | 页面级 SettingsCarPropertyManager(监听+SET) | L1 缓存 + L2 回放 |
| GLOVE_BOX_UNAVL_REASON | 0x6140B40A / 1631630346 | GloveBoxController 按需读 | 页面级,getProperty 读时自动注册回调 | 读后入 L1 缓存 |
| GLOVE_BOX_BUTTON_STATUS | 0x61401781 / 1631590273 | 物理按键(Meego 5458412,联调中) | VehiclePermanentListener 常驻监听 | 全局静态 map,进程生命周期 |
1. 属性通道三层:读/写/缓存的代码实证
1.1 L1 页面层:每个控制器一个 SettingsCarPropertyManager
页面基座是 CarPropertyMgrPreferenceController。它在 onCreateInternal 里 new 一个属于自己页面生命周期的属性管理器(W:base/settingsVehicleLib/.../common/CarPropertyMgrPreferenceController.java:510-519):
mCarPropertyManager = new SettingsCarPropertyManager(getContext(),
getFragmentController().getSettingsLifecycle());
mCarPropertyManager.setHolderName(getImplClassName() + "@" + Objects.hashCode(this));onPostCreateInternal 再把自己设为回调(:521-527)。生命周期销毁时,SettingsCarPropertyManager 内部的 CarLifecycleObserver.onDestroy 自动 release()(W:SettingsCarPropertyManager.kt:119-138)——所以页面级监听离开页面就死,这是”常驻监听”必须另起炉灶的根因(见 §2)。
GloveBoxController 声明它关心的属性(W:GloveBoxController.kt:99-102):
override fun getPropertyIdSet() = ArraySet<Int>().apply {
add(MiCarPropertyIds.DrivingCtrl.GLOVE_BOX)
add(Driving.CAR_MODE_STATUS)
}注册发生在视图绑定完成时:BaseVehicleButtonGroupPropController.onCreateInternal 里 setViewBindedListener 回调调 registerPropertyCallback()(W:BaseVehicleButtonGroupPropController.java:49-59)。
读路径:controller 的 getProperty/getIntProperty 全部委托给 mCarPropertyManager(W:CarPropertyMgrPreferenceController.java:78-95)。SettingsCarPropertyManager.getProperty 先查本页缓存,未命中走 remoteGetProperty(W:SettingsCarPropertyManager.kt:378-385、:398-412):
private fun <T> remoteGetProperty(propId: Int, areaId: Int): MiCarPropertyValue<T>? {
if (!mCarPropMgr.isConnected) {
return null
}
return mCarPropMgr.getProperty<T>(propId, areaId)?.also {
MLog.d("remoteGetProperty ${it.toTinyString()}")
cachePropVal(it)
if (!mObservedPropertyIdSet.contains(propId)) {
// 自动注册callback,callback之后缓存
mCarPropMgr.registerPropertyCallback(mEventCallbackDispatcher, propId)
// 注册的propId需要记录一下,用于解注册
mNoNeedObservedPropertyIdSet.add(propId)
}
}
}注意这个读时自动注册:GLOVE_BOX_UNAVL_REASON 没有出现在 getPropertyIdSet() 里,但点击不可用按钮时 getIntProperty(Peripheral.GLOVE_BOX_UNAVL_REASON)(W:GloveBoxController.kt:289)会把该 propId 自动注册进回调并记入 mNoNeedObservedPropertyIdSet——“读一次即开始收听”是框架内建行为,台架注入该属性后 Toast 能跟着变就是这个原因。
写路径:setVehicleProperty → mCarPropertyManager.setCarProperty(...)(W:CarPropertyMgrPreferenceController.java:606-646)。setCarProperty 按值类型分发(Int/Float/IntArray/List 批量),底层异步形式先扔工作线程再同步调 binder(W:VehicleControlManager.java:128-132),最后 startTimeOutMonitor 启动超时监测(W:SettingsCarPropertyManager.kt:429-487)。开箱动作实证(W:GloveBoxController.kt:222-229):
private fun openGloveBox(openType: Int = TrackValues.GLOVE_BOX_OPEN_NO_PASSWORD) {
MLog.tag(TAG).d("openGloveBox openType=$openType")
setVehicleProperty(
MiCarPropertyIds.DrivingCtrl.GLOVE_BOX,
CommonParams.OpenClose.OPEN
)
reportGloveBoxOpenTrack(openType)
}L1 自己的缓存:mPropertiesCache 是 HashMap<String, MiCarPropertyValue<*>>,key=propId#areaId(:99、:583-585);cachePropVal 有两个特殊点——首终态值替换(VehiclePropertyConfig.getPropValueForFirstCache)与 mCallback?.isIgnoreCache 放行(:593-616)。controller 侧 isIgnoreCache 默认”中间态不缓存”(W:CarPropertyMgrPreferenceController.java:410-414),防止 UI 回退到中间态。
1.2 L2 全局缓存分发:CarPropertyCacheManager(进程单例)
页面管理器的注册最终落到 VehicleControlManager.registerPropertyCallback,它不直接注册到服务,而是转给 L2(W:VehicleControlManager.java:70-78):
CarPropertyCacheManager.getInstance()
.registerPropertyCallback(mMiCarServiceManager, callback, propertyId);L2 的核心数据结构(W:CarPropertyCacheManager.java:36-43):mCallBackMap(propId → CopyOnWriteArraySet 回调集合)、mPropertiesCache(propId#areaId → 值)、mAreasSet(propId → 已见 areaId)、mErrorEvents(错误事件缓存)。
注册回放段(:159-194,本篇最重要的 20 行):某 propId 第一次注册时才向远端服务注册(first-time register pId:%s to remote service,:187、:192-193);之后再有人注册同一 propId,只进本地集合,然后逐 areaId 用缓存值回放一次 onChangeEvent(:197-228 callbackByCache),若缓存里有错误事件则回放 onErrorEvent(:175-178):
private void callbackByCache(MiCarPropertyManager carPropertyManager,
MiCarPropertyManager.MiCarPropertyEventCallback callback, int propertyId, int area,
String key) {
// 取出缓存的CarPropertyValue
MiCarPropertyValue value = mPropertiesCache.get(key);
...
// 如果本地没有,尝试从service获取一次
if (value == null) {
value = carPropertyManager.getProperty(propertyId, area);
...
}
try {
...
callback.onChangeEvent(value); // callback from local cache这就是”页面一开就有当前值”的机理:首注册回放 + 缓存缺时向服务补读一次。远端事件到达时(:61-107),L2 先写缓存、清错误事件、记录 areaId,再把事件同步分发给该 propId 的全部回调(异常被 try/catch 兜住,不会一个回调崩全链路)。分发前 logChangeEvent 把所有回调名拼起来打一条日志(:109-129),日志里 controller名|controller名 onChangeEvent ... 的格式就出自这里——台架对日志时可用它确认”这个属性有几个订阅者”。
1.3 L3 服务连接层与重连
VehicleControlManager extends NewBaseMiCarPropertyServiceManager<MiCarPropertyManager>(W:VehicleControlManager.java:52),服务名 MiCar.PROPERTY_SERVICE(:61-68)。连接状态由 OnMiCarServiceListener 上报,SettingsCarPropertyManager.setCarManagerCallback 注册该监听(W:SettingsCarPropertyManager.kt:308-326):
mCarPropMgr.registerCarServiceListener(object :
NewBaseMiCarPropertyServiceManager.OnMiCarServiceListener {
override fun onCarServiceManagerPrepared() {
mCallback?.onPropertyManagerPrepared()
}
override fun onCarServiceManagerReleased() {
mCallback?.onPropertyManagerReleased()
onServiceDisconnect()
}
})- prepared(服务就绪/重连恢复):controller 的
onPropertyManagerPrepared置mIsCarServiceReady=true并重新registerPropertyCallback()(W:CarPropertyMgrPreferenceController.java:293-304);BaseVehicleButtonGroupPropController.onHandlePropertyManagerPrepared还会filterSupportCtrls()+refreshUi()(W:BaseVehicleButtonGroupPropController.java:162-176)。重连后全量重注册+重刷新是框架行为,不用业务自己写。 - released(服务断开):
onServiceDisconnect()清超时消息、清 L1 缓存(W:SettingsCarPropertyManager.kt:227-234);controller 置mIsCarServiceReady=false、mHasRegistered=false(:306-311),下次 prepared 才能重新注册。
非页面调用方(如 CarConfigManager)没有生命周期,走 L0 全局入口 GlobalCarPropertyManager:进程级 object,内部用 SoftReference<SettingsCarPropertyManager> 持有一个共享管理器(W:GlobalCarPropertyManager.kt:16-30),软引用被回收则 getCarManager() 里惰性重建(:32-38)。它在进程启动时由 MainThreadStartTask.execute() 初始化(W:app/.../MainThreadStartTask.java:62,同段 :66 初始化 VehiclePermanentListener、:67 挂上 CarConfigManager)。
2. VehiclePermanentListener:常驻监听为什么选它
物理按键(GLOVE_BOX_BUTTON_STATUS,Meego 5458412)这类信号的要求是:设置页面没打开时也得收。页面级管理器随生命周期 release(§1.1),显然不行。VehiclePermanentListener 的设计正好对位(W:base/settingsVehicleLib/.../miauto/VehiclePermanentListener.java):
- 无生命周期绑定,永不自动 release:
init()里new SettingsCarPropertyManager(mAppContext, null)(:95-100),lifecycle 传 null,CarLifecycleObserver挂不上,管理器活到进程死。 - prepared 重注册覆盖服务重连:
onPropertyManagerPrepared()里对getPropertyIdSet()全量重注册,并把重连期间积压的待下发信号(mWaitingVehicleDataList)补发出去(:103-115)。服务重启不死订阅。 - CopyOnWrite 分发,订阅者随时进出:
mCarPropertyManagerCallbackList是CopyOnWriteArrayList(:69),handleVehiclePropertyChanged尾部forEach分发(:393),遍历时增删安全。真实消费者例子:TrackTrigger.kt:42-43用它监听 POWER_MODE 在 ACC 时触发埋点模块。 - 注册即得当前值:
addCarPropertyManagerCallback在服务已连接时立即回调一次onPropertyManagerPrepared()(:305-313),新订阅者不用等下一个变化事件。 - 全局静态缓存供显隐判断:
mPropertyValueMap(:66)注释写明”用于 Preference 在 onCreate() 时判断显示隐藏,避免闪烁”,对外是getCachePropertyValue()(:319-337)——先查缓存,缓存没有且服务已连才去读。
进程启动顺序钉死在 MainThreadStartTask.execute()(W:MainThreadStartTask.java:55-77):GlobalCarPropertyManager.init(:62)→ VehiclePermanentListener.getInstance().init()(:66)→ MiCarSettings.setConfigManager(CarConfigManager.INSTANCE)(:67)。这三个必须在任何页面打开前完成,否则首帧显隐判定无数据。
物理按键的完整链路(按键 → VehiclePermanentListener → 拉起密码页/开箱)见 07-物理按键扩展设计;propId 与值域见 01-需求与信号契约 与 08-速查卡。
3. SET 语义与「台架回滚假象」
3.1 框架的 SET 后置校验
setCarProperty 成功后启动超时监测(W:SettingsCarPropertyManager.kt:548-569):
- 只对本控制器已观察的 propId 启动(:553,
not observed in $mHolderName, don't need start timeout monitor); - 超时值由
VehiclePropertyConfig.getMappingTimeoutPropId/getCustomTimeoutValue映射; - 到时未收到匹配回执,
handlePropertyTimeout干两件事(:193-203):回调onSetPropertyTimeout,并比对下发值与当前缓存值,不一致再打onDataVerifyFailed propId: ..., propVal: ...日志并回调。
controller 侧默认动作是 restorePropertyToPrevious——用最新缓存值重刷 UI(W:CarPropertyMgrPreferenceController.java:125-133、:392-408);onDataVerifyFailed 默认弹配置出来的 Toast(:710-716)。此外 SET 后等待窗内(isInHMITriggerWaitingDuration)的同值回包会被 shouldIgnorePropertyFeedback 屏蔽(:689-691),防自回弹——这是正常行为,别误读成丢包。
3.2 台架为什么必然”回滚”(2026-10-19 mach 开箱实测)
台架 vehicled 不持久化 App 的 SET:SET OPEN 后 vehicled 仍按(真实值或 mock 值)继续发布,App 超时校验回读不一致,UI 被 restorePropertyToPrevious 弹回。mach 台架无密码开箱的日志签名(场景文档 §4,逐行对应上面的代码):
GloveBoxController #onTabClicked() on tab: Tab[打开手套箱, false, true] ← mIsSelected=false, available=true
CarSettings-GloveBoxController: no password, open glove box ← getLocalPwd() 空 → 无密码路径
CarSettings-GloveBoxController: openGloveBox openType=1 ← type=1 无密码
CarSettings: VehicleControlReporter reportOpenGloveBox type=1 ← 埋点写完即报,不等回执
CarSettings: >>> Done set propVal: 1 to propId: 0x6140b407 ← SET OPEN 成功(App 侧)
onDelayGetProp ... onDataVerifyFailed propVal: 1 → 回读 v=0 ← 超时校验,缓存仍是 mock/真实值 0
末行就是 SettingsCarPropertyManager.kt:200 那条日志。铁律:判定以 App 侧日志(>>> Done set propVal)为准;UI 回滚/回读 0 是台架假象,真车(无 mock)不会发生。(KNOWLEDGE §9 / LESSONS #10)
解法(2026-10-14 mach 实证,KNOWLEDGE §9):
adb shell settings put --user 0 GLOBAL vehicle_mock_data 1 # 底层持久化 SET,UI 不再被弹回
# ... 用毕必须显式复位:
adb shell settings put --user 0 GLOBAL vehicle_mock_data 0注意两点:vehicle_mock_data 存在 settings provider,重启不自动清,复位必须显式执行;开 1 后 lshal --get 仍读到 mock 基值——调试读值通道与 App 可见值语义分离,别拿 --get 当 UI 判据。
4. 台架 mock 操作手册(通道/root/节奏/复位)
4.1 通道判法:先分清 AIDL 还是 HIDL
| 形态 | 判别命令 | 注入命令 |
|---|---|---|
| AIDL(新车机,fermi 实证) | dumpsys -l | grep -i vehicle 有 IVehicle/micar | dumpsys android.hardware.automotive.vehicle.IVehicle/micar --mock_from_car <propId> -a <area> --stat 0 -i <val> |
| HIDL(老车机,mach/ampere 实证) | 上条没有 → lshal | grep -i vehicle | lshal debug android.hardware.automotive.vehicle@2.0::IVehicle/default --mock_from_car <propId> -a 0 --stat 0 -i <val> |
-a <area>:全局信号=0;--stat 0=状态置 AVAILABLE。propId 在命令里用十进制更稳(KNOWLEDGE §2/§11)。
4.2 root 是硬门槛,且会掉
- SELinux 按 adbd 上下文放行 VHAL:
u:r:shell:s0访问被avc: denied { find }拒,报FAILED_TRANSACTION/does not exist, or no permission to connect;u:r:su:s0(adb root)放行(LESSONS 8,KNOWLEDGE §8)。 - 每批 mock 前先查:
adb shell "id | grep -o 'context=[^ ]*'",不是su:s0就adb root。 - 整机 reboot、adbd 重启都会掉 root——“昨天行今天不行”先查 context,别瞎 reboot。
4.3 注入节奏与值变化序列
- 间隔 ≥0.25s:~16 条密集连发会把 VHAL 通道打失联(LESSONS #9),脚本内置
sleep 0.25。 - 值变化序列(LESSONS #11):只翻 status 不改值,App 不刷新——框架按值变化去抖,且 L2 缓存回放的是旧值。每个信号注入都带变化序列(如真实值 1 → 注 2 已构成变化;需要反复触发时 0→1→0)。2026-10-13 fermi 实证:
--stat 0 -i 2(值=台架真实值)后页面仍暗,补 2→1→2 序列后 tab 全亮。 - 每轮
am force-stop com.android.car.settings再开页(LESSONS #2):CCP 配置字走 CarConfigManager 进程级静态缓存,mock 新值不重读(日志签名hit cached pId: ..., pVal: <旧值>);force-stop 后回首页,导航链必须从头走(LESSONS #14)。 - mock 是全局污染(LESSONS #16):影响所有读该 prop 的进程,不止被测 App。
4.4 复位铁律
- 开测前先记录每个 propId 原始读值;
- 末轮逐条注回真实值;
vehicle_mock_data若开过必须显式归 0(重启不清);wrong_times等状态 key 保持 0(达上限残留本身是异常态,不恢复);- 再开一次页面确认基线。
5. 手套箱配方全录(mach 台架实证)
信号组(propId 经 javap 编译产物解析实证,权威;详见 01-需求与信号契约):
| 信号 | dec | hex | mock 目标 | 作用 |
|---|---|---|---|---|
| GLOVE_BOX_LOCK_TYPE_CONFIG | 1631630345 | 0x6140B409 | 2(POWER) | 入口显示唯一门控;真实值 1(MANUAL) 整组隐藏 |
| GLOVE_BOX | 1631630343 | 0x6140B407 | 无需(mock 用真实值即可) | status 驱动 isAvailable;value 0=CLOSE/1=OPEN 驱动 mIsSelected |
| GLOVE_BOX_UNAVL_REASON | 1631630346 | 0x6140B40A | 按需 | 0=IDLE/1=NOT_DIS_ARMED/2=ANTY_PLAY;不可用时 Controller 读它弹 Toast |
| GLOVE_BOX_BUTTON_STATUS | 1631590273 | 0x61401781 | 物理按键联调时用 | 0=ERROR/1=NOT_PRESSED/2=PRESSED/3=NO_INFO;语义上仅 NOT_PRESSED→PRESSED 边沿算一次按下 |
5.1 入口最小集:LOCK_TYPE_CONFIG 1→2
V="lshal debug android.hardware.automotive.vehicle@2.0::IVehicle/default"
adb -s $D shell "$V --mock_from_car 1631630345 -a 0 --stat 0 -i 2"; sleep 0.25
adb -s $D shell "am force-stop com.android.car.settings"变化本身已触发下发;force-stop 清 CarConfigManager 静态缓存是必需。恢复注回 -i 1(MANUAL)。本场景纯显隐/点击链路,未开 vehicle_mock_data。门控代码实证(W:VehicleControlSettingsFragment.java:168-172):
// 非电动手套箱 屏蔽功能
if (Peripheral.GloveBoxLockTypeConfig.POWER
!= CarConfigManager.INSTANCE.getGloveBoxLockType()) {
ids.add(R.string.pk_vehicle_control_glove_box);
}getGloveBoxLockType 默认 MANUAL(W:CarConfigManager.kt:139-146);其 getIntProperty 带 INVALID_CONFIG_VAL = Int.MIN_VALUE 哨兵探测服务可用性(:63、:87-111)——“信号不存在(老 ROM)“与”信号值=合法值”是两条路径,自测要分别覆盖。
5.2 导航与开箱
monkey 拉起设置 → 左栏「车辆控制」(坐标不缓存,现 dump 现点,LESSONS #12)→ input swipe 1000 1500 1000 500 300 滚底 → 出现「手套箱」分组+「打开手套箱」卡片 → 点击:无密码直接 SET OPEN(日志签名见 §3.2);有密码弹密码页;错误达上限弹车主扫码验证。
5.3 密码页 root 直拉(绕过 UI 设密码)
GloveBoxPasswordDialog(exported=false、singleInstance、taskAffinity=:GloveBoxPassword),shell 上下文 am start 被 Permission Denial 拒;root 后 ActivityStarter 跳过 exported 检查(LESSONS #18):
adb -s $D root
adb -s $D shell "am start -n com.android.car.settings/.miauto.vehicle.glovebox.GloveBoxPasswordDialog --ei param_event_id 0 --ez param_is_close_pwd false -f 0x10000000"param_event_id:0=CHECK_PWD 验证 / 1=SET_PWD 设置 / 2=CHANGE_PWD 修改 / 3=已验证后修改,等价PinPasswordCheckUtils.goToCheckPassword。- 直拉没有注册密码结果 LocalBroadcast 回调:输完密码会发
com.android.car.settings.action.PASSWORD_VERIFY_RESULT但无人接收,后续开箱动作不会发生——要全链路就走真实入口。 - 密码设置页同理:
GloveBoxSettingsDialogActivity(也 exported=false)。
5.4 wrong_times 清零(LESSONS #17)
密码页一开就是「扫码验证车主身份」?多半是残留:
adb -s $D shell "settings list global | grep glove_box" # 查 glove_box_password_wrong_times
adb -s $D shell "settings put global glove_box_password_wrong_times 0"
adb -s $D shell "am force-stop com.android.car.settings"wrong_times ≥ 5(上限)时 CHECK_PWD 页一打开就被 BasePinPasswordFragment 转车主账号扫码验证,密码键盘根本没机会显示。判别:uiautomator 只有「扫码验证车主身份」无 0-9 键盘节点。演示完保持 0。
5.5 其余门槛速记
- 密码设置入口需账号登录(未登录只有「打开手套箱」),台架未登录属正常,演示密码页用 §5.3 直拉。
- 未设密码时输任何 4 位都失败并累加 wrong_times;要演示「输对→开箱」须先写合法
glove_box_password(SHA-256(password+salt),Settings.Global 跟车存储)。 - 扫码页=车主账号验证,默认要 P 挡;密码页内「忘记密码」传
isNeedCheckGear=false不限挡。
6. mock 注入 → App 感知 → UI 更新全链路时序
sequenceDiagram autonumber participant OP as 台架操作者(adb) participant VHAL as VHAL/vehicled participant CS as CarService<br/>(MiCarPropertyManager) participant L2 as CarPropertyCacheManager<br/>(L2 缓存分发) participant L1 as SettingsCarPropertyManager<br/>(L1 页面管理器) participant CTL as GloveBoxController participant UI as 车辆控制页 OP->>VHAL: --mock_from_car 1631630345 -a 0 --stat 0 -i 2 Note over VHAL,CS: 去抖点1:值不变不发布<br/>(必须值变化序列) VHAL->>CS: publish(LOCK_TYPE_CONFIG=2) CS->>L2: onChangeEvent L2->>L2: 写 mPropertiesCache<br/>(缓存点2;新注册者回放 callbackByCache) Note over OP,CTL: CarConfigManager 进程级静态缓存不随推送刷新<br/>→ 必须 am force-stop 再开页 OP->>CTL: am force-stop → 重新开车辆控制页 CTL->>L1: 开页读 getGloveBoxLockType() L1->>CS: remoteGetProperty(缓存未命中) CS-->>L1: 2(POWER) L1-->>CTL: 2 CTL->>UI: 门控放行,手套箱分组显示 OP->>UI: 点击「打开手套箱」 CTL->>L1: setVehicleProperty(GLOVE_BOX, OPEN) L1->>CS: SET 0x6140B407 = 1 L1->>L1: startTimeOutMonitor<br/>(去抖点3:等待窗内同值回包被屏蔽) Note over L1,CTL: 台架 vehicled 不持久 SET<br/>超时回读≠下发值 → onDataVerifyFailed<br/>(判定以 ">>> Done set propVal" 为准) CS-->>L1: 回执/周期发布(值回 0) L1->>L1: cachePropVal(缓存点1:中间态不缓存) L1->>CTL: onPropertyChanged CTL->>UI: 更新 isAvailable / mIsSelected 刷新
7. 证据清单
| 文件(相对 W 根) | 行号 | 说明 |
|---|---|---|
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/GlobalCarPropertyManager.kt | :16-30, :32-38, :40-54 | L0 全局入口、SoftReference 持有、回调扇出 |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/SettingsCarPropertyManager.kt | :99-102, :119-138, :193-203, :239-257, :308-326, :378-412, :429-487, :548-569, :593-616, :673-696 | L1 页面管理器:缓存/生命周期 release/超时与 onDataVerifyFailed/注册/prepared 通知/读时自动注册/SET 分发/首终态值/事件分发 |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/CarPropertyCacheManager.java | :36-43, :61-107, :159-194, :197-228 | L2 单例:四张表、远端事件缓存+分发、首注册到远端、缓存回放 callbackByCache |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehicleControlManager.java | :52, :61-68, :70-88, :128-132, :175-177 | L3 服务连接;注册转 L2;SET 异步转同步;getProperty 直读服务 |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/VehiclePermanentListener.java | :66, :69, :95-115, :274-303, :305-313, :319-337, :362-394, :436-444 | 常驻监听:无生命周期、prepared 重注册+补发、CopyOnWrite 分发、注册即 prepared、全局静态缓存 |
| app/src/main/java/com/android/car/settings/miauto/MainThreadStartTask.java | :55-77 | 进程启动初始化三件套(GlobalCarPropertyManager/VehiclePermanentListener/CarConfigManager) |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/common/CarPropertyMgrPreferenceController.java | :78-95, :125-133, :269-311, :392-414, :510-527, :606-646, :689-691, :710-716 | 页面基座:读委托/回滚/注册与 prepared/release/超时错误默认动作/建管/SET/回显屏蔽/校验失败 Toast |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/BaseVehicleButtonGroupPropController.java | :49-59, :162-176 | 视图绑定完成即注册;prepared 后 filterSupportCtrls+refreshUi |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/CarConfigManager.kt | :57, :63, :76, :87-111, :139-146 | CCP 进程级缓存、哨兵探测、getGloveBoxLockType |
| settingsPage/VehicleBodyControl/…/vehicle/VehicleControlSettingsFragment.java | :168-172 | 手套箱入口 POWER 门控 |
| settingsPage/VehicleBodyControl/…/vehicle/glovebox/GloveBoxController.kt | :99-102, :166-188, :222-229, :261-302 | 监听集合、属性驱动 UI、开箱 SET、四分支点击、不可用原因读值 |
| settingsPage/micarSnapshot/…/snapshot/TrackTrigger.kt | :42-43 | VehiclePermanentListener 真实消费者(POWER_MODE ACC 触发埋点) |
| base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehiclePropertyUtil.java | :36-43, :173-182, :248-250 | mock 环境开关、isPropertyAvailable(status==AVAILABLE)、信号丢失 status 4/5 |
| /home/zbc/下载/test/车机JIRA/.claude/skills/car-signal-mock-test/KNOWLEDGE.md | §1-2, §8-11 | 链路原理、AIDL/HIDL 判法、SELinux root、vehicled SET 语义与 vehicle_mock_data、缓存三机制、javap 解析 |
| /home/zbc/下载/test/车机JIRA/.claude/skills/car-signal-mock-test/LESSONS.md | 18 | root 门槛、force-stop、值翻转、注入间隔、回滚假象、值变化序列、全局污染、wrong_times、root 直拉 |
| /home/zbc/下载/test/车机JIRA/.claude/skills/car-signal-mock-test/references/场景-手套箱入口与密码页.md | 全文 | 手套箱信号组 propId、入口最小集、开箱日志签名(2026-10-19 mach)、密码页直拉、复位清单 |
相关篇目:01-需求与信号契约(四个属性的值域与权限)、08-速查卡(一页纸命令)、04-密码与账号验证体系(密码状态机)、mock 通用方法论另见 能量回收知识库 的台架篇。