手套箱 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 key | pk_vehicle_control_glove_box_toggle | res/values/micar_preference_keys.xml:82 |
| 标题 | micar_vehicle_control_glove_box = “手套箱” | values-zh-rCN/strings.xml:170 |
| Controller | glovebox.GloveBoxController | XML L226 |
| 页面位置 | HvacAppSeatEntrance 之后、电动踏板 easy_in_out 之前 | XML L210-232 |
| Tab 索引 | 开关=0,密码设置=1 | GloveBoxController.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,输错时显示。
- 4 个密码位指示器(38dp×62dp),最后一位显示明文数字,其余显示圆点
(
- 弹窗互斥:两个 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 两处。