手套箱 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 方法论来自共享 skill car-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_CONFIG0x6140B409 / 1631630345Fragment 显隐门控 → CarConfigManager.getGloveBoxLockType()CarConfigManager → GlobalCarPropertyManager进程级静态缓存,mock 后必须 force-stop
GLOVE_BOX0x6140B407 / 1631630343GloveBoxController页面级 SettingsCarPropertyManager(监听+SET)L1 缓存 + L2 回放
GLOVE_BOX_UNAVL_REASON0x6140B40A / 1631630346GloveBoxController 按需读页面级,getProperty 读时自动注册回调读后入 L1 缓存
GLOVE_BOX_BUTTON_STATUS0x61401781 / 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):

  1. 无生命周期绑定,永不自动 release:init() 里 new SettingsCarPropertyManager(mAppContext, null)(:95-100),lifecycle 传 null,CarLifecycleObserver 挂不上,管理器活到进程死。
  2. prepared 重注册覆盖服务重连:onPropertyManagerPrepared() 里对 getPropertyIdSet() 全量重注册,并把重连期间积压的待下发信号(mWaitingVehicleDataList)补发出去(:103-115)。服务重启不死订阅。
  3. CopyOnWrite 分发,订阅者随时进出:mCarPropertyManagerCallbackList 是 CopyOnWriteArrayList(:69),handleVehiclePropertyChanged 尾部 forEach 分发(:393),遍历时增删安全。真实消费者例子:TrackTrigger.kt:42-43 用它监听 POWER_MODE 在 ACC 时触发埋点模块。
  4. 注册即得当前值:addCarPropertyManagerCallback 在服务已连接时立即回调一次 onPropertyManagerPrepared()(:305-313),新订阅者不用等下一个变化事件。
  5. 全局静态缓存供显隐判断: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/micardumpsys android.hardware.automotive.vehicle.IVehicle/micar --mock_from_car <propId> -a <area> --stat 0 -i <val>
HIDL(老车机,mach/ampere 实证)上条没有 → lshal | grep -i vehiclelshal 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 复位铁律

  1. 开测前先记录每个 propId 原始读值;
  2. 末轮逐条注回真实值;
  3. vehicle_mock_data 若开过必须显式归 0(重启不清);
  4. wrong_times 等状态 key 保持 0(达上限残留本身是异常态,不恢复);
  5. 再开一次页面确认基线。

5. 手套箱配方全录(mach 台架实证)

信号组(propId 经 javap 编译产物解析实证,权威;详见 01-需求与信号契约):

信号dechexmock 目标作用
GLOVE_BOX_LOCK_TYPE_CONFIG16316303450x6140B4092(POWER)入口显示唯一门控;真实值 1(MANUAL) 整组隐藏
GLOVE_BOX16316303430x6140B407无需(mock 用真实值即可)status 驱动 isAvailable;value 0=CLOSE/1=OPEN 驱动 mIsSelected
GLOVE_BOX_UNAVL_REASON16316303460x6140B40A按需0=IDLE/1=NOT_DIS_ARMED/2=ANTY_PLAY;不可用时 Controller 读它弹 Toast
GLOVE_BOX_BUTTON_STATUS16315902730x61401781物理按键联调时用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-54L0 全局入口、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-696L1 页面管理器:缓存/生命周期 release/超时与 onDataVerifyFailed/注册/prepared 通知/读时自动注册/SET 分发/首终态值/事件分发
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/CarPropertyCacheManager.java:36-43, :61-107, :159-194, :197-228L2 单例:四张表、远端事件缓存+分发、首注册到远端、缓存回放 callbackByCache
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehicleControlManager.java:52, :61-68, :70-88, :128-132, :175-177L3 服务连接;注册转 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-146CCP 进程级缓存、哨兵探测、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-43VehiclePermanentListener 真实消费者(POWER_MODE ACC 触发埋点)
base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehiclePropertyUtil.java:36-43, :173-182, :248-250mock 环境开关、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.md18root 门槛、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 通用方法论另见 能量回收知识库 的台架篇。