手套箱 01 · 功能与页面结构

本篇回答:手套箱功能是什么、入口在哪、什么人/什么车能看见。 基线:fuben31_micarsettings/MiCarSettings @ dev

1. 功能全景

手套箱功能由两块组成:

子功能说明入口
电动开箱点击按钮 → 车控属性写 OPEN → 手套箱电动弹开车辆控制页左按钮「打开手套箱」
密码手套箱可选的 4 位数字 PIN;设密码后开箱需先验证;错 5 次锁定,需车主账号验证解锁车辆控制页右按钮「手套箱密码设置」

两个按钮在 UI 上是一个 Preference 里的双 Tab(ButtonIconTogglePreference), 由同一个 GloveBoxController 管理。

flowchart LR
    subgraph 车辆控制页
        GB["手套箱 Category<br/>pk_vehicle_control_glove_box"]
        GB --> T0["Tab0 打开手套箱<br/>ic_glove_box_switch<br/>TAB_INDEX_GLOVE_BOX_SWITCH=0"]
        GB --> T1["Tab1 手套箱密码设置<br/>ic_glove_box_settings<br/>TAB_INDEX_GLOVE_BOX_PASSWORD_SET=1"]
    end
    T0 -->|点击| Open["开箱分支<br/>(02 篇)"]
    T1 -->|点击| Settings["GloveBoxSettingsDialogActivity<br/>密码管理弹窗"]

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>
项值出处
Preference keypk_vehicle_control_glove_box_toggleres/values/micar_preference_keys.xml:82
标题micar_vehicle_control_glove_box = “手套箱”values-zh-rCN/strings.xml:170
Controllerglovebox.GloveBoxControllerXML L226
页面位置HvacAppSeatEntrance 之后、电动踏板 easy_in_out 之前XML L210-232
Tab 索引开关=0,密码设置=1GloveBoxController.kt:73,76

3. 三道门控:谁能看见/谁能点

手套箱入口经过三层递进门控,任何一层不过都不会到达用户:

flowchart TD
    Q1{"① 配置字门控<br/>GLOVE_BOX_LOCK_TYPE_CONFIG<br/>== POWER(2)?"}
    Q1 -->|否: MANUAL(1)或读不到| Hide1["整类 Preference 从页面移除<br/>VehicleControlSettingsFragment.java:143-147"]
    Q1 -->|是| Show["入口 Category 显示"]
    Show --> Q2{"② 登录门控(Tab1 可见性)<br/>小米账号已登录?"}
    Q2 -->|未登录| Hide2["Tab1(密码设置)隐藏<br/>GloveBoxController.kt:86-96"]
    Q2 -->|已登录| Vis["Tab1 可见"]
    Vis --> Q3{"③ 模式门控(Tab1 可用性)<br/>非代客 && CarMode==NORMAL?"}
    Q3 -->|代客/非正常| Dis["Tab1 置灰,点击 Toast<br/>glove_box_fail_valet_mode<br/>GloveBoxController.kt:181-186,134"]
    Q3 -->|正常| OK["Tab1 可点"]

3.1 ① 配置字门控(硬件级,决定 Category 显隐)

settingsPage/VehicleBodyControl/src/main/java/com/android/car/settings/miauto/vehicle/VehicleControlSettingsFragment.java:143-147:

// 非电动手套箱 屏蔽功能
if (Peripheral.GloveBoxLockTypeConfig.POWER
        != CarConfigManager.INSTANCE.getGloveBoxLockType()) {
    ids.add(R.string.pk_vehicle_control_glove_box);
}
  • 配置字来源:CarConfigManager.getGloveBoxLockType() (base/settingsVehicleLib/.../CarConfigManager.kt:133-140),读属性 MiCarPropertyIds.DrivingCtrl.GLOVE_BOX_TYPE(= Peripheral.GLOVE_BOX_LOCK_TYPE_CONFIG, 0x6140b409), 默认值 MANUAL(1) —— 即读不到信号时默认隐藏。
  • 枚举(SDK mi/car/config/Peripheral.java:1430-1433):MANUAL=1 手动手套箱 / POWER=2 电动手套箱。
  • 这是车型差异的唯一决定点:源码编译进所有 flavor(dcd / xcd_cn_a14 / xcd_global_a14 / xcd_cn_a17 / xcd_global_a17,见 settingsPage/VehicleBodyControl/build.gradle:11-32), 运行时由配置字决定显隐,不存在按车型白名单的硬编码。

3.2 ② 登录门控(Tab1 可见性)

GloveBoxController.kt:86-96 + 广播刷新 :305-332:

private fun setGloveBoxPwdSettingEntryVisibility() {
    val visible = AccountUtils.isUserLogin(...)
    // Tab1.isVisible = visible
    refreshUi()
}
  • 未登录小米账号时,Tab1(密码设置入口)整体隐藏;Tab0(开箱)不受影响。
  • 实时刷新:注册账号广播 com.mi.car.account.USER_DATA_CHANGED 与 android.accounts.LOGIN_ACCOUNTS_POST_CHANGED(action 值见 CarSettingsJump.kt:370-375), 登录/登出即时显隐。API 33+ 带 RECEIVER_EXPORTED + mi.car.account.permission.ACCESS_BROADCAST (GloveBoxController.kt:318-325)。
  • ⚠️ 历史包袱:这两笔修复(“手套箱密码设置未及时显示” part1/part2,55ddcf397/5a2f3b2ee, 2025-10/12)就是补的 Application Context + 登录广播监听——之前入口状态不刷新。

3.3 ③ 模式门控(Tab1 可用性 + Tab0 Toast 兜底)

GloveBoxController.kt:181-186:

val carMode = getIntProperty(Driving.CAR_MODE_STATUS)
val isValetMode = AccountUtils.isValetMode(context)
gloveBoxSetTab?.isAvailable = !isValetMode && carMode == Driving.CarModeStatus.NORMAL
  • 代客模式判定:AccountUtils.isValetMode() 读 Settings.Secure["judge_user_guest"] == 1 (AccountUtils.java:217-221)。
  • 代客/非正常模式下 Tab1 置灰,点击 Toast glove_box_fail_valet_mode「请在用户模式下使用该功能」 (GloveBoxController.kt:133-135)。
  • Tab0(开箱)不可用时点击,Toast 由车控不可用原因驱动(见 04 篇 GLOVE_BOX_UNAVL_REASON 语义表):未解锁「请先解锁车辆」/防玩「操作过于频繁」/代客兜底 (GloveBoxController.kt:288-302)。

4. 密码管理弹窗(GloveBoxSettingsDialogActivity)

点击 Tab1 后弹出的不是新页面,而是一个 Dialog 风格的 Activity (GloveBoxSettingsDialogActivity,继承 BaseConflictDialogActivity):

  • 布局:res/layout/glovebox_pwd_settings_dialog_activity.xml —— 一个「密码开关」行 (PreCheckSwitch + 标题 + 副标题) + 一个「修改密码」行 + 底部按钮区(代码中 GONE,L108)。
  • 状态刷新 updateUIState()(GloveBoxSettingsDialogActivity.kt:213-223): Switch 的开关态不看车辆属性,只看 Settings.Global 里有没有密码哈希:
    val hasPwd = !GloveBoxPasswordTools.getLocalPwd().isNullOrEmpty()
    if (!hasPwd) { mPwdChange.visibility = GONE;  mPwdSwitch.setChecked(false) }
    else         { mPwdChange.visibility = VISIBLE; mPwdSwitch.setChecked(true) }
  • Switch 被 PreCheckSwitch.setPreCheckInterceptor 拦截(L71-79,isPrevent 恒 true), 点击不会直接翻转,统一走 handleSwitchChange() 分支(详见 02 篇),并叠加 isDoubleClick() 防抖(L75)。
flowchart LR
    subgraph 密码管理弹窗
        S["密码开关行<br/>PreCheckSwitch(拦截式)"]
        C["修改密码行<br/>hasPwd 时才 VISIBLE"]
    end
    S -->|点击| H{handleSwitchChange<br/>GloveBoxSettingsDialogActivity.kt:145}
    H -->|未设密码| Set["SET_PWD 设置流程"]
    H -->|已设密码| Clear["CHECK_PWD+isClosePwd<br/>验证后 clearPwd"]
    H -->|已锁定| AV["账号验证"]
    C -->|点击| Chg["CHANGE_PWD 修改流程"]

5. 密码输入弹窗(GloveBoxPasswordDialog + Fragment)

  • GloveBoxPasswordDialog(glovebox/GloveBoxPasswordDialog.kt:31-85)只是宿主 Activity (singleInstance,AndroidManifest.xml:32-40),布局 activity_glove_box_pwd_dialog.xml 里只有一个 FragmentContainerView; showFragment()(L72-78)先 GloveBoxAccountVerifyUtils.cancelVerify() 清残留验证, 再加载 GloveBoxPasswordFragment。
  • GloveBoxPasswordFragment(glovebox/GloveBoxPasswordFragment.kt:22)继承通用 BasePinPasswordFragment,真正的键盘 UI 在 baseui 的 res/layout/fragment_pin_pwd_dialog.xml:
    • 4 个密码位指示器(38dp×62dp),最后一位显示明文数字,其余显示圆点 (BasePinPasswordFragment.kt:388-410);
    • 数字键 1-9/0(104dp×104dp 圆形)+ 删除键 + 底部「忘记密码」;
    • 错误提示 error_notice 默认 gone,输错时显示。
  • 弹窗互斥:两个 Activity 都是冲突弹窗(isConflictDialog()=true), GloveBoxSettingsDialogActivity 通过 conflictExceptSource() 放行 GloveBoxPasswordDialog(L241-243),允许密码框叠在设置弹窗上; 其它冲突弹窗出现时会互踢 finish(BaseConflictDialogActivity.kt:27-48)。

6. 遗留死代码清单(读代码时别被误导)

文件状态说明
glovebox/GloveBoxButton.kt + layout/micar_glove_box_btn.xml无调用方2025-02 UI 重构前的自定义按钮,实际渲染由 ButtonIconTogglePreference 父类完成
layout/fragment_glove_box_pwd_dialog.xml遗留资源手套箱专属键盘布局;2026-04 迁移通用框架后,实际使用 baseui fragment_pin_pwd_dialog.xml
base/settingsBaseUi/.../ui/NumberKeyboardView.java手套箱未使用从手套箱早期 UI 抽取的通用键盘 View(Javadoc L21 自述),现仅服务锁屏 PIN 页(LockPinVerifyFragment/LockPinSetFragment)
PageAlias.DIALOG_GLOVE_BOX_CONTROL / DIALOG_GLOVE_BOX_SET_CONTROL有定义、仓内无调用路由别名 "glovebox_pw" / "glovebox_pw_set"(PageAlias.kt:58-59),预留给外部 deeplink

7. 同平台复用提醒

通用 PIN 框架 base/settingsBaseUi/src/main/java/com/android/car/settings/common/password/ (PinPasswordTools / PinPasswordConfig / PinPasswordCheckUtils / PinAccountVerifyUtils / BasePinPasswordFragment)由 2026-04-03 commit fde7fbfbb/“a064a6528” 建立, 手套箱与升降顶舱(Roof,micarLockSettings)共用。手套箱侧只是参数化:

glovebox/GloveBoxPasswordTools.kt:33-38:

private val config = PinPasswordConfig(
    settingsKeyPassword = "glove_box_password",
    settingsKeyWrongTimes = "glove_box_password_wrong_times",
    settingsKeySalt = "glove_box_password_salt",
    broadcastAction = GloveBoxPasswordFragment.ACTION_PASSWORD_VERIFY_RESULT
)
val impl: PinPasswordTools by lazy { PinPasswordTools(config) }

改框架行为(PIN 长度、锁定阈值、错误提示、流程)前先确认影响面: grep -rl "PinPasswordTools\|BasePinPasswordFragment" settingsPage/ 至少会命中 glovebox 与 roof 两处。