手套箱 02 · 页面结构与门控

回答一个问题:手套箱入口挂在哪个页面、由什么控制显示/可用/可见、两个 Tab 的三态语义。

基线声明

  • 主锚点(W):/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。

前置阅读:00-手套箱-README(功能全景);属性值语义表见 01-需求与信号契约。

0. TL;DR

  • 入口挂在 设置 → 车辆控制(VehicleControlSettingsFragment),XML 声明为一个 MiCarPreferenceCategory 分组,内嵌一个 ButtonIconTogglePreference,Controller 是 GloveBoxController。
  • 三道门控,自上而下:①配置字门控——GLOVE_BOX_LOCK_TYPE_CONFIG != POWER(2) 时整组从页面移除;②信号状态门控——GLOVE_BOX 属性的 status 决定「打开手套箱」Tab 是否可用;③账号/代客门控——账号未登录时「密码设置」Tab 直接 GONE,代客模式或非 NORMAL 车模式下置灰。
  • 两个 Tab 的三态(isVisible / isAvailable / mIsSelected)分别映射到 View 的 VISIBLE/GONE、isEnabled + alpha、isSelected,渲染收口在 ButtonIconAdapter。
  • 页面不开,Controller 不建、属性不订阅、广播不注册——手套箱的全部逻辑随页而生灭,这是物理按键需求必须另起结构的根因(见第 5 节)。

1. 页面挂载链:XML key → Fragment → Controller → Tab

1.1 挂载链总览

flowchart TD
    A["XML: miauto_vehicle_body_ctrl_settings_fragment.xml<br/>Category key=pk_vehicle_control_glove_box"] --> B["Fragment: VehicleControlSettingsFragment<br/>getPreferenceScreenResId 加载 XML"]
    B --> C{"onCreatePreferences<br/>removePreferencesIfNeed()"}
    C -->|"配置字!=POWER, key在移除列表"| D["整组 Category 被 removePreference<br/>Controller 不会实例化"]
    C -->|"配置字==POWER, 保留"| E["遍历 mPreferenceControllers<br/>按 key 绑定 Preference"]
    E --> F["GloveBoxController 实例化<br/>(XML settings:controller 声明)"]
    F --> G["initLocalVehicleControls()<br/>注册两个 Tab"]
    G --> H["Tab0: 打开手套箱<br/>createGloveBoxSwitchCtrl"]
    G --> I["Tab1: 手套箱密码设置<br/>createGloveBoxPasswordSetCtrl"]

1.2 XML 声明

settingsPage/VehicleBodyControl/src/main/res/xml/miauto_vehicle_body_ctrl_settings_fragment.xml:219-229:

<!--  密码手套箱  -->
<com.android.car.settings.miauto.preferences.MiCarPreferenceCategory
    android:key="@string/pk_vehicle_control_glove_box"
    android:title="@string/micar_vehicle_control_glove_box">
    <com.android.car.settings.miauto.preferences.ButtonIconTogglePreference
        android:key="@string/pk_vehicle_control_glove_box_toggle"
        android:layout="@layout/micar_ui_pref_button_layout"
        settings:controller="com.android.car.settings.miauto.vehicle.glovebox.GloveBoxController" />
    <com.android.car.settings.miauto.preferences.MiCarSpaceDividerPreference
        android:key="@string/pk_vehicle_control_glove_box_divider"/>
</com.android.car.settings.miauto.preferences.MiCarPreferenceCategory>

要点:

  • 门控作用于外层 Category(pk_vehicle_control_glove_box),不是内层 toggle。移除 Category 等于把标题、按钮、分割线一锅端。
  • Controller 通过 settings:controller 属性静态声明在 XML 里,键值字符串定义于 settingsPage/VehicleBodyControl/src/main/res/values/micar_preference_keys.xml:80-82(pk_vehicle_control_glove_box / _divider / _toggle)。

1.3 Fragment 与 key 过滤机制

宿主 Fragment 是 VehicleControlSettingsFragment(W:settingsPage/VehicleBodyControl/src/main/java/com/android/car/settings/miauto/vehicle/VehicleControlSettingsFragment.java):

  • :70 @RouterProvider(path = VEHICLE_CONTROL_URI_PATH) —— 路由「车辆控制」页;
  • :83-87 getPreferenceScreenResId() 返回 R.xml.miauto_vehicle_body_ctrl_settings_fragment;
  • :98 getPreferenceKeyResIdsToRemove() 返回本页要隐藏的 preference key 资源 id 列表。

过滤机制的执行点在基类 BaseXmlParserSettingsFragment(W:base/settingsBaseUi/src/main/java/com/android/car/settings/common/BaseXmlParserSettingsFragment.java):

  • :647-667 onCreatePreferences():addPreferencesFromResource 建树 → 遍历 mPreferenceControllers 按 key 绑定 Preference(:656-660)→ :662 removeHiddenList() → :663 removePreferencesIfNeed()。
  • :1201-1209 removePreferencesIfNeed():拿到 getPreferenceKeyResIdsToRemove() 列表,逐个 removePreference(preferenceKeyResId)。
  • :1211-1245 removePreference():非哨兵 id 走 preferenceScreen.findPreference(getContext().getString(preferenceKeyResId)) 再 doRemovePreference;哨兵 PREF_KEY_RES_ID_CHECK_EMPTY_CATEGORY(:120)用于「组内删空后连 Category 一起删」的收尾。

注意 :1184-1188 的注释约束:getPreferenceKeyResIdsToRemove() 也会被语音搜索 APT 反射调用,此时 Context 不可用——所以手套箱门控①里读的 CarConfigManager.INSTANCE.getGloveBoxLockType() 走的是带缓存的静态读(见 2.1),不依赖 Fragment Context。

1.4 Controller 创建与两个 Tab 注册

GloveBoxController(W:settingsPage/VehicleBodyControl/src/main/java/com/android/car/settings/miauto/vehicle/glovebox/GloveBoxController.kt):

  • :54-64 继承 BaseVehicleButtonIconToggleGroupPropController;
  • :111-138 initLocalVehicleControls() 先 mSupportedCtrls.clear(),再依次 add 两个控件,各自带 withOnClickListener:
mSupportedCtrls.add(VehicleBodyPropertyMap.createGloveBoxSwitchCtrl()
    .withOnClickListener { tab: Tab<*> ->
        MLog.d("%s #onTabClicked() on tab: %s", implClassName, tab)
        if (tab.isAvailable) {
            showGloveBoxSwitchClick(tab)
        } else {
            showGloveBoxSwitchUnavailableNotify()
        }
    }
)
 
mSupportedCtrls.add(VehicleBodyPropertyMap.createGloveBoxPasswordSetCtrl()
    .withOnClickListener { tab: Tab<*> ->
        MLog.d("%s #onTabClicked() on tab: %s", implClassName, tab)
        if (tab.isAvailable) {
            context.startActivity(
                Intent(context, GloveBoxSettingsDialogActivity::class.java)
            )
        } else {
            MiCarToastUtils.showToast(R.string.glove_box_fail_valet_mode)
        }
    }
)

两个 Tab 的工厂在 VehicleBodyPropertyMap.kt(W:同模块 vehicle/VehicleBodyPropertyMap.kt):

  • :42-50 createGloveBoxSwitchCtrl():属性 MiCarPropertyIds.DrivingCtrl.GLOVE_BOX,图标 ic_glove_box_switch,文案 R.string.glove_box_switch(中文「打开手套箱」,values-zh-rCN/strings.xml:251),默认值 CommonParams.OpenClose.OPEN,带 withAllowClickWhenDisabled(true) 与语音 flag TAB_FLAG_GLOVE_BOX_SWITCH(TabKeyMapHelper.java:24)。
  • :55-63 createGloveBoxPasswordSetCtrl():同一属性 ID,图标 ic_glove_box_settings,文案 R.string.glove_box_pwd_set(「手套箱密码设置」,values-zh-rCN/strings.xml:252),默认值 false,flag TAB_FLAG_GLOVE_BOX_PASSWORD_SET(TabKeyMapHelper.java:25)。

withAllowClickWhenDisabled(true) 是关键:禁用态下点击事件依然会上抛,所以上面两个 listener 的 else 分支才有机会弹 Toast 解释「为什么不能用」。字段定义见 CarUiTabLayout.java:760,渲染侧透传见 ButtonIconAdapter.kt:168。

2. 三道门控逐一拆解

2.1 门控① 配置字 LOCK_TYPE_CONFIG == POWER,否则整组移除

代码证据(W:VehicleControlSettingsFragment.java:168-172):

// 非电动手套箱 屏蔽功能
if (Peripheral.GloveBoxLockTypeConfig.POWER
        != CarConfigManager.INSTANCE.getGloveBoxLockType()) {
    ids.add(R.string.pk_vehicle_control_glove_box);
}

配置字读取在 CarConfigManager.kt:139-146(W:base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/CarConfigManager.kt):

fun getGloveBoxLockType(): Int {
    val res = getIntProperty(
        MiCarPropertyIds.DrivingCtrl.GLOVE_BOX_TYPE,
        Peripheral.GloveBoxLockTypeConfig.MANUAL
    )
    MLog.tag(TAG).d("getGloveBoxLockType: $res")
    return res
}

常量定义(平台源码 vendor/micar/packages/services/MiVehicleService/support-lib/src/mi/car/config/Peripheral.java):

  • :596 GLOVE_BOX_LOCK_TYPE_CONFIG = 0x409(属性低位 ID;完整 ID 形如 0x6140b40x 段,同前缀的 GLOVE_BOX=0x6140b407 见 00-README 第 24 行);
  • :1421-1424 GloveBoxLockTypeConfig { MANUAL = 0x1; POWER = 0x2 }。

映射关系:MiCarPropertyIds.java:1487 DrivingCtrl.GLOVE_BOX_TYPE = Peripheral.GLOVE_BOX_LOCK_TYPE_CONFIG。

语义与默认值:默认 MANUAL(1)=机械手套箱=不显示入口;只有读出 POWER(2)(电动手套箱)才保留。读不到(服务不可用)时 getIntProperty 返回默认值(CarConfigManager.kt:100-106 的 INVALID_CONFIG_VAL 探测逻辑),即「读不到就藏」——安全方向是隐藏。

台架验证方法(2026-10-19 mach 实证:mock 2 后入口出现):

  1. 台架注入配置字 0x409 = 2(POWER);
  2. adb shell am force-stop com.android.car.settings(清缓存重读);
  3. 重进 设置 → 车辆控制,「手套箱」分组出现;
  4. 反向 mock 0x409 = 1(MANUAL)重复上述步骤,分组消失。 判定以 App 侧实际渲染为准(uiautomator dump 查 pk_vehicle_control_glove_box 或截图);vehicled 不持久,重启回滚是预期行为。

2.2 门控② GLOVE_BOX.status 驱动开关 Tab 的 isAvailable

代码证据(W:GloveBoxController.kt:166-188,onHandlePropertyChange):

// 更新手套箱开关状态
val gloveBoxStatus = getProperty(MiCarPropertyIds.DrivingCtrl.GLOVE_BOX)
val gloveBoxSwitchTab: Tab<*>? = preference.buttons[TAB_INDEX_GLOVE_BOX_SWITCH]
MLog.tag(TAG).d("onPropertyChanged: gloveBoxStatus=$gloveBoxStatus")
gloveBoxSwitchTab?.isAvailable = VehiclePropertyUtil.isPropertyAvailable(gloveBoxStatus)
gloveBoxSwitchTab?.mIsSelected = VehiclePropertyUtil
    .getIntPropertyValue(gloveBoxStatus) == CommonParams.OpenClose.OPEN

isPropertyAvailable 的判定(W:base/settingsVehicleLib/src/main/java/com/android/car/settings/miauto/vehicle/VehiclePropertyUtil.java:173-182):属性对象非空、未被 mock 置不可用、且 getStatus() == MiCarPropertyValue.STATUS_AVAILABLE。

属性订阅集合在 :99-102:GLOVE_BOX + Driving.CAR_MODE_STATUS 两个。属性 ID 映射:MiCarPropertyIds.java:1485 DrivingCtrl.GLOVE_BOX = Peripheral.GLOVE_BOX,平台侧 :576 GLOVE_BOX = 0x407。

不可用时的点击分支(:288-302 showGloveBoxSwitchUnavailableNotify)按 Peripheral.GLOVE_BOX_UNAVL_REASON 选文案:

  • NOT_DIS_ARMED(1) → glove_box_fail_need_unlock(需先解锁车辆);
  • ANTY_PLAY(2) → glove_box_fail_anty_play(防玩模式);
  • 其他 → 代客模式给 glove_box_fail_valet_mode「请在用户模式下使用该功能」(values-zh-rCN/strings.xml:245),否则不弹。

枚举值定义见平台 Peripheral.java:1426-1430(IDLE=0 / NOT_DIS_ARMED=1 / ANTY_PLAY=2)。

台架验证方法:注入 0x407 属性 status 为不可用 → 开关 Tab 置灰(alpha 降为 DISABLE 档,渲染见 3.2);点击应弹对应 reason 的 Toast;恢复 status 可用 → 置灰解除。reason 属性分别注入 1/2 验证两条文案。

2.3 门控③ 账号登录驱动密码 Tab isVisible;代客+CAR_MODE 驱动其 isAvailable

密码入口同时受两套条件管,可见性与可用性分离:

可见性(W:GloveBoxController.kt:86-96):

private fun setGloveBoxPwdSettingEntryVisibility() {
    val gloveBoxSetTab: Tab<*>? = preference.buttons[TAB_INDEX_GLOVE_BOX_PASSWORD_SET]
    if (AccountUtils.isUserLogin(BaseApplication.getGlobalApp().applicationContext)) {
        MLog.i(TAG, "Account login, showing glove box password setting entry")
        gloveBoxSetTab?.isVisible = true
    } else {
        MLog.i(TAG, "Account logout, hiding glove box password setting entry")
        gloveBoxSetTab?.isVisible = false
    }
    refreshUi()
}

调用时机有两个:onStartInternal()(:150-153,页面每次回到前台刷一次)和账号广播接收器(:305-332 registerReceiver,监听 USER_DATA_CHANGED="com.mi.car.account.USER_DATA_CHANGED" 与 LOGIN_ACCOUNTS_POST_CHANGED="android.accounts.LOGIN_ACCOUNTS_POST_CHANGED",常量见 CarSettingsJump.kt:370-371,权限 mi.car.account.permission.ACCESS_BROADCAST 见 :375;API 33+ 走带 RECEIVER_EXPORTED 标志的注册重载,:320-325)。

可用性(:181-186,在 onHandlePropertyChange 内):

if (TAB_INDEX_GLOVE_BOX_PASSWORD_SET in preference.buttons.indices) {
    val carMode = getIntProperty(Driving.CAR_MODE_STATUS)
    val isValetMode = AccountUtils.isValetMode(context)
    val gloveBoxSetTab: Tab<*>? = preference.buttons[TAB_INDEX_GLOVE_BOX_PASSWORD_SET]
    gloveBoxSetTab?.isAvailable = !isValetMode && carMode == Driving.CarModeStatus.NORMAL
}

AccountUtils(W:base/settingsBaseLib/src/main/java/com/android/micar/settings/common/AccountUtils.java)::217-221 isValetMode 读 Settings.Secure 的访客标记;:224-226 isUserLogin = getXiaomiAccount(context) != null。

不可用时的点击反馈固定为 glove_box_fail_valet_mode Toast(:134)。

台架验证方法:

  • 可见性:退出小米账号 → 密码 Tab 从布局消失(GONE);重新登录 → 重现。广播路径可用「退出/登录账号」真实操作触发,无需 mock。
  • 可用性:进入代客模式 → 密码 Tab 置灰、点击弹「请在用户模式下使用该功能」;退出代客且 CAR_MODE_STATUS == NORMAL → 恢复可点。

3. Tab 三态语义表

三态字段定义在 CarUiTabLayout.Tab(W:base/settingsBaseUi/src/main/java/com/android/car/settings/miauto/common/CarUiTabLayout.java):

  • :708 protected Boolean isAvailable = true;
  • :753 public boolean isVisible = true;
  • :754 public boolean mIsSelected;
态定义处手套箱 Tab0(开关)驱动源手套箱 Tab1(密码)驱动源UI 表现(渲染处)
isVisibleCarUiTabLayout.java:753恒 true(代码未改它)账号登录=true / 登出=false(GloveBoxController.kt:86-96)ButtonIconAdapter.kt:68-72:true→View.VISIBLE,false→View.GONE,占位整块移除
isAvailableCarUiTabLayout.java:708GLOVE_BOX 属性 status==AVAILABLE(GloveBoxController.kt:176;VehiclePropertyUtil.java:173-182)!isValetMode && carMode==NORMAL(GloveBoxController.kt:185)ButtonIconAdapter.kt:165 buttonIconView.isEnabled = tab.isAvailable;:169-173 不可用→alpha 降为 DISABLE 档(置灰)
mIsSelectedCarUiTabLayout.java:754属性值==OpenClose.OPEN 时 true(GloveBoxController.kt:177-178)不用(默认 false,VehicleBodyPropertyMap.kt:60)ButtonIconAdapter.kt:166-167 同步到 View 的 isSelected;:121-127 选中且有 checkedText 时换文案

三态正交,可任意组合。两个实战组合:

  • 已开箱:Tab0 isAvailable=true + mIsSelected=true → 高亮可选中态,点击走 showGloveBoxSwitchClick 第一分支弹「请手动关闭手套箱」(:262-263,文案 values-zh-rCN/strings.xml:249)。手套箱只有「开」没有「关」的下发,关箱是物理动作。
  • 未登录:Tab1 isVisible=false → 直接 GONE,此时它的 isAvailable 是多少都无所谓,用户根本看不到。

4. 布局与标题资源

  • 2 列布局:GloveBoxController.kt:70 private val TAB_COLUMN_SIZE: Int = 2;onCreateInternal(:140-148)里经 preference.config.putInt(PreferenceArgs.PREFERENCE_TAB_ITEM_COLUMN_SIZE, TAB_COLUMN_SIZE) 下发。注意 :142-144 与 :146 对同一个 key 重复 putInt 了两次,无害但属冗余。Tab 索引常量::73 TAB_INDEX_GLOVE_BOX_SWITCH = 0、:76 TAB_INDEX_GLOVE_BOX_PASSWORD_SET = 1。
  • 单 Tab 布局模板:基类 BaseVehicleButtonIconToggleGroupPropController.kt:29-40 在 configPreference() 里把 PREFERENCE_TAB_ITEM_LAYOUT_RES 定为 R.layout.micar_ui_button_icon_toggle_layout、权重 ITEM_WEIGHT_WRAP_CONTENT。
  • 分组标题::145 preference.assistViewTitle = getString(R.string.micar_vehicle_control_glove_box)(语音「可见即可说」用标题);XML 侧 Category title 同为该资源(miauto_vehicle_body_ctrl_settings_fragment.xml:222)。多语言:values/strings.xml:171 英文「Glovebox」;values-zh-rCN/strings.xml:170 中文「手套箱」。

5. 「页面不开则逻辑不活」——生命周期随页

Controller 的一生完全绑在 Preference 树上:

sequenceDiagram
    participant F as "VehicleControlSettingsFragment"
    participant Base as "BaseXmlParserSettingsFragment"
    participant C as "GloveBoxController"
    participant V as "车辆属性服务"

    F->>Base: onCreatePreferences()
    Base->>Base: addPreferencesFromResource(XML)
    alt 门控①命中(非电动手套箱)
        Base->>Base: removePreference(pk_vehicle_control_glove_box)
        Note over C: 不会实例化, 全程不存在
    else 门控①放行
        Base->>C: 实例化 + setPreference()
        C->>C: onCreateInternal(): initButtons/config/列数/registerReceiver
        Note over C: View 绑定后才 registerPropertyCallback<br/>(BaseVehicleButtonGroupPropController.java:55-58)
        V-->>C: 属性上报(GLOVE_BOX / CAR_MODE_STATUS)
        C->>C: onStartInternal(): 刷密码入口可见性
        F->>C: 页面销毁 onDestroyInternal(): unregisterReceiver
    end

关键事实:

  • 属性回调注册发生在 View bind 完成之后(BaseVehicleButtonGroupPropController.java:55-58 的 setViewBindedListener),不是 Controller 一建就订阅;
  • 账号广播在 onCreateInternal 注册、onDestroyInternal 注销(GloveBoxController.kt:147、:155-158),页面销毁即停;
  • 门控①命中时 Category 被移除,Controller 连实例化都不会发生。

结构性推论:在这套范式里,「设置页不开,手套箱逻辑就是死的」。任何要求页面关闭时也要响应的能力(典型如物理按键开箱、语音在任意界面唤醒后的状态查询)都不能寄生在这个 Controller 里,必须另起一个独立于页面生命周期的受理端。这正是物理按键扩展的设计动机,展开见 07-物理按键扩展设计。语音/远程已走的独立通道(ModulePublishService)参见 04-密码与账号验证体系。

6. 门控判定全流程图

从「入口是否出现」到「点击走哪条分支」的完整判定:

flowchart TD
    S["用户进入 设置→车辆控制"] --> G1{"门控① LOCK_TYPE_CONFIG==POWER(2)?<br/>VehicleControlSettingsFragment.java:168-172"}
    G1 -->|"否(MANUAL/读不到)"| X1["整组移除<br/>页面无手套箱入口"]
    G1 -->|"是"| R["渲染两个 Tab"]
    R --> C1{"点击哪个 Tab?"}
    C1 -->|"Tab0 打开手套箱"| G2{"门控② tab.isAvailable?<br/>(GLOVE_BOX.status)"}
    G2 -->|"否"| T1["Toast: 按 UNAVL_REASON 选文案<br/>未解锁/防玩/代客"]
    G2 -->|"是"| S1{"mIsSelected?<br/>(已开?)"}
    S1 -->|"已开"| T2["Toast: 请手动关闭手套箱"]
    S1 -->|"未开"| P1{"本地有密码?"}
    P1 -->|"无"| O1["直接下发 OPEN 开箱"]
    P1 -->|"有"| P2{"错误次数达上限?"}
    P2 -->|"是"| O2["车主账号验证页(扫码)<br/>通过后清零+开箱"]
    P2 -->|"否"| O3["密码输入弹窗<br/>验证通过开箱"]
    C1 -->|"Tab1 密码设置"| G3{"门控③a isVisible?<br/>(账号已登录)"}
    G3 -->|"否(未登录)"| X2["Tab 已 GONE<br/>不存在点击"]
    G3 -->|"是"| G4{"门控③b isAvailable?<br/>(非代客 && CAR_MODE==NORMAL)"}
    G4 -->|"否"| T3["Toast: 请在用户模式下使用该功能"]
    G4 -->|"是"| O4["跳 GloveBoxSettingsDialogActivity"]

开箱后的密码生命周期(设置/修改/关闭/忘记密码)不在本篇范围,见 04-密码与账号验证体系;点击埋点(54662 等)见 05-埋点体系。

7. 出货对照

GloveBoxController.kt 在 W 与出货快照 pack_a17_cn_20260923 之间 diff 为空、逐字节一致(含 :318-331 的 API 33 RECEIVER_EXPORTED 注册段)。本篇全部行号对出货线同样适用。

证据清单

文件(相对 W 根或注明绝对路径)行号区间说明
settingsPage/VehicleBodyControl/…/vehicle/VehicleControlSettingsFragment.java70, 83-87, 98-99, 168-172路由、XML 加载、key 移除列表、手套箱门控①
settingsPage/VehicleBodyControl/src/main/res/xml/miauto_vehicle_body_ctrl_settings_fragment.xml219-229手套箱 Category + ButtonIconTogglePreference + controller 声明
settingsPage/VehicleBodyControl/src/main/res/values/micar_preference_keys.xml80-82三个 preference key 字符串
base/settingsBaseUi/…/common/BaseXmlParserSettingsFragment.java120, 647-667, 1190-1245key 过滤执行链(removePreferencesIfNeed/removePreference)
base/settingsVehicleLib/…/miauto/CarConfigManager.kt100-106, 139-146getGloveBoxLockType 读配置字,读不到回默认 MANUAL
(平台)vendor/micar/…/mi/car/config/Peripheral.java576, 596, 1421-1424, 1426-1430GLOVE_BOX=0x407、LOCK_TYPE_CONFIG=0x409、MANUAL/POWER、UNAVL_REASON 枚举
base/settingsBaseLib/…/miauto/vehicle/MiCarPropertyIds.java1485-1487DrivingCtrl.GLOVE_BOX / GLOVE_BOX_TYPE 映射
settingsPage/VehicleBodyControl/…/vehicle/glovebox/GloveBoxController.kt54-64, 70-76, 86-102, 111-158, 166-188, 261-302, 305-332本篇主角:双 Tab 注册、三道门控、生命周期、广播注册
settingsPage/VehicleBodyControl/…/vehicle/VehicleBodyPropertyMap.kt42-50, 55-63两个 Tab 的工厂(图标/文案/默认值/tabFlag)
base/settingsBaseUi/…/miauto/voiceassist/TabKeyMapHelper.java24-25TAB_FLAG_GLOVE_BOX_SWITCH / PASSWORD_SET
base/settingsVehicleLib/…/vehicle/BaseVehicleButtonGroupPropController.java49-59, 162-176onCreateInternal 序列、View bind 后才注册属性回调
base/settingsVehicleLib/…/vehicle/BaseVehicleButtonIconToggleGroupPropController.kt14-41单 Tab 布局模板与权重
base/settingsBaseUi/…/miauto/common/CarUiTabLayout.java708, 753-754, 760, 1066-1079Tab 三态字段定义与 setter
base/settingsBaseUi/…/miauto/common/adapter/ButtonIconAdapter.kt68-72, 121-127, 164-173三态到 View 的渲染收口(VISIBLE/GONE、enable+alpha、selected)
base/settingsVehicleLib/…/vehicle/VehiclePropertyUtil.java173-182isPropertyAvailable 判定(STATUS_AVAILABLE)
base/settingsBaseLib/…/micar/settings/common/AccountUtils.java217-226isValetMode / isUserLogin
base/settingsBaseUi/…/common/pageroute/CarSettingsJump.kt370-375账号广播 action 与权限常量
settingsPage/VehicleBodyControl/src/main/res/values/strings.xml / values-zh-rCN/strings.xml171 / 170, 245, 249, 251-252标题与 Toast 多语言文案
(出货对照)pack_a17_cn_20260923 同路径 GloveBoxController.kt全文件与 W diff 为空,逐字节一致