手套箱 02 · 开箱业务流与密码生命周期(核心篇)
本篇回答:用户每点一下,系统到底走哪条路。 基线:
fuben31_micarsettings/MiCarSettings@ dev 主入口:GloveBoxController.showGloveBoxSwitchClick()(GloveBoxController.kt:261-278)
1. 开箱总状态机
stateDiagram-v2 [*] --> 点击开箱 点击开箱 --> 已开提示: tab.mIsSelected=true<br/>(属性已是 OPEN) 已开提示 --> [*]: Toast glove_box_close_notice<br/>「请手动关闭手套箱」 点击开箱 --> 不可用提示: isAvailable=false 不可用提示 --> [*]: 按 UNAVL_REASON Toast<br/>(未解锁/防玩/代客) 点击开箱 --> 判定密码: 可用且未开 判定密码 --> 直接开箱: 未设密码<br/>getLocalPwd 空 判定密码 --> 账号验证: 已锁定<br/>错误次数 ≥ 5 判定密码 --> 密码验证: 已设密码未锁定 密码验证 --> 直接开箱: 验证通过 账号验证 --> 直接开箱: 验证通过<br/>(错误次数清零) 直接开箱 --> [*]: setVehicleProperty<br/>GLOVE_BOX=OPEN(1)<br/>+ 埋点 54662
分发代码(GloveBoxController.kt:261-278):
private fun showGloveBoxSwitchClick(tab: Tab<*>) {
if (tab.mIsSelected) { // 已是开
MiCarToastUtils.showToast(R.string.glove_box_close_notice)
} else {
if (GloveBoxPasswordTools.getLocalPwd().isNullOrEmpty()) {
openGloveBox() // ① 无密码直开
} else if (GloveBoxPasswordTools.isPwdLimit()) {
showAccountVerifyPage() // ② 锁定→账号验证
} else {
showPasswordCheckDialog() // ③ 密码验证
}
}
}注意判定顺序:先查密码有无,再查锁定。锁定状态只在设过密码时才有意义。
2. 三分支时序图(设置页开箱)
sequenceDiagram participant U as 用户 participant C as GloveBoxController participant T as PinPasswordTools participant D as GloveBoxPasswordDialog<br/>+BasePinPasswordFragment participant AV as AccountVerifyHelper participant V as vehicled U->>C: 点击「打开手套箱」 alt 分支① 无密码(getLocalPwd 空) C->>V: setVehicleProperty(GLOVE_BOX, OPEN) Note over C: GloveBoxController.kt:265-268<br/>不引导设密码,直接开 C->>C: 埋点 type=1(无密码直开) else 分支② 有密码未锁定 C->>D: goToCheckPassword(CHECK_PWD)<br/>startActivity(GloveBoxPasswordDialog) D->>U: 4 位数字键盘 U->>D: 输入 4 位(200ms 后自动校验) D->>T: verifyPwd(): SHA-256 比对 alt 密码正确 T-->>D: callback(true) D-->>C: LocalBroadcast ACTION_PASSWORD_VERIFY_RESULT=true C->>V: setVehicleProperty(GLOVE_BOX, OPEN) C->>C: 埋点 type=2(密码开) else 错误(第 1~4 次) T->>T: wrong_times+1 写 Settings.Global D->>U: error_notice「密码错误」 else 第 5 次错误 T-->>D: callback(false, isLimit=true) D->>AV: 自动拉起账号验证<br/>(Fragment 内,不查 P 挡场景见 §5) AV-->>D: 通过→清零+RESULT=true D-->>C: 开箱 end else 分支③ 已锁定(wrong_times≥5) C->>AV: GloveBoxAccountVerifyUtils.openVerifyPage()<br/>默认先查 P 挡 alt 非 P 挡 AV-->>U: Toast「请驻车后重试」 else P 挡 AV->>AV: contentResolver.call(content://mi.car.account,<br/>"confirm", scene=glovebox_verify, style=qr_code) U->>AV: 手机扫码完成车主验证 AV-->>C: callback(true) C->>T: setPwdWrongTimes(0) C->>V: setVehicleProperty(GLOVE_BOX, OPEN) C->>C: 埋点 54662(type=2) + 54663(忘记密码成功) end end
3. 密码生命周期
3.1 存储方式(确证)
不是 SharedPreferences,不是私有文件,是 Settings.Global——跟车存储,恢复出厂即清除:
| 数据 | Key | 内容 | 出处 |
|---|---|---|---|
| 密码哈希 | glove_box_password | SHA-256(salt + "[1, 2, 3, 4]"格式) 十六进制 | GloveBoxPasswordTools.kt:34;PinPasswordTools.kt:266-275 |
| 盐值 | glove_box_password_salt | 时间戳毫秒(putLong),每次保存新密码重新生成,旧哈希自动失效 | GloveBoxPasswordTools.kt:36;PinPasswordEncryptor.kt:113-132 |
| 错误次数 | glove_box_password_wrong_times | 明文 int | GloveBoxPasswordTools.kt:35;PinPasswordTools.kt:323-330 |
细节:输入的 4 位数字先转 IntArray.contentToString()(形如 "[1, 2, 3, 4]")再拼盐哈希
(PinPasswordTools.kt:239)。验证 = 同样算法算一遍与存储值比对,不明文比较。
3.2 四个密码操作的分支矩阵
| 操作 | 入口 | 输入模式 | 流程 | 成功动作 | 埋点 |
|---|---|---|---|---|---|
| 设置密码 | 设置弹窗 Switch(未设密码时点击) | INPUT_EVENT_SET_PWD | 输入 4 位 → 200ms 后第二次输入 → 两次一致 | savePwd() 写哈希+新盐;Toast「设置成功」;Switch 置 ON、显示「修改密码」 | 54664 type=1 |
| 关闭密码 | 设置弹窗 Switch(已设密码时点击) | INPUT_EVENT_CHECK_PWD + isClosePwd=true | 输一次旧密码验证 | clearPwd() 清哈希+清零错误;Toast「清除成功」;Switch 置 OFF、隐藏「修改密码」 | 54664 type=2 |
| 修改密码 | 设置弹窗「修改密码」行 | INPUT_EVENT_CHANGE_PWD | 先验旧密码 → 通过后两次新密码 | savePwd();Toast「修改成功」 | 54664 type=3 |
| 忘记密码 | 密码弹窗底部「忘记密码」(仅 CHECK/CHANGE 模式显示) | 账号验证 → INPUT_EVENT_SET_PWD | 扫码验证(跳过 P 挡检查)→ 通过后直接进设置新密码 | 新密码保存;Toast「修改成功」 | 54663(在 Controller/Activity 回调处另报) |
流程出处:
- 设置:
GloveBoxSettingsDialogActivity.kt:153-164;基类 SET 模式BasePinPasswordFragment.kt:231-236 - 关闭:
GloveBoxSettingsDialogActivity.kt:170-183;clearPwd()在PinPasswordTools.kt:282-290 - 修改:
GloveBoxSettingsDialogActivity.kt:85-99,118-135;旧密码阶段BasePinPasswordFragment.kt:237-243 - 忘记密码:
BasePinPasswordFragment.kt:262-276(openAccountVerifyPage(false)跳过 P 挡)
flowchart TD subgraph SET["设置(未设密码)"] S1[输入 4 位] --> S2[200ms 后第二次输入] S2 --> S3{两次逐位一致?} S3 -->|是| S4[savePwd: 新盐+SHA256<br/>写 Settings.Global] S3 -->|否| S5[error: 两次输入不一致<br/>重新输] end subgraph CLEAR["关闭(已设密码)"] C1[输入旧密码] --> C2{正确?} C2 -->|是| C3[clearPwd: 哈希置空<br/>错误次数清零] C2 -->|否| C4[wrong_times+1] end subgraph CHANGE["修改"] M1[验旧密码] --> M2{正确?} M2 -->|是| M3[切 SET 模式<br/>标题「请设置新密码」] M3 --> M4[两次新密码 → savePwd] end subgraph FORGOT["忘记密码"] F1[点「忘记密码」] --> F2[账号验证<br/>isNeedCheckGear=false] F2 --> F3{扫码通过?} F3 -->|是| F4[mIsForgetPassword=true<br/>隐藏忘记按钮,进 SET] F4 --> M4 end
3.3 错误与锁定机制
PinPasswordTools.kt:62-71 四个核心常量:
private const val PWD_WRONG_MAX_TIMES = 5 // 锁定阈值
private const val PWD_MAX_LENGTH = 4 // PIN 长度
private const val DELAY_TIMES = 200L // 输完自动校验延迟(ms)
private const val PWD_EMPTY_VALUE = -1 // 空位标记verifyPwd()(PinPasswordTools.kt:238-258):
private fun verifyPwd() {
val res = PinPasswordEncryptor.hashPassword(input, salt) == getLocalPwd()
if (!res) {
val times = getPwdWrongTimes() + 1
setPwdWrongTimes(times)
if (times >= PWD_WRONG_MAX_TIMES) { // 第 5 次错
reset()
verifyCallback?.invoke(false, true) // isLimit=true
return
}
} else {
setPwdWrongTimes(0) // 成功清零
}
reset()
verifyCallback?.invoke(res, false)
}锁定后的行为(三条路都先账号验证):
flowchart TD L[已锁定 wrong_times≥5] --> P1{入口} P1 -->|点开箱按钮| E1[showAccountVerifyPage<br/>查 P 挡] P1 -->|设置弹窗 Switch| E2[showAccountVerify<br/>查 P 挡] P1 -->|点「修改密码」| E3[先账号验证 → 通过后<br/>CHANGE_PWD_VERIFIED<br/>跳过旧密码验证] P1 -->|密码弹窗内第 5 次输错| E4[Fragment 自动拉起账号验证<br/>BasePinPasswordFragment.kt:316-326] E1 & E2 & E3 & E4 --> OK{验证通过?} OK -->|是| R[setPwdWrongTimes 0<br/>继续原操作] OK -->|否| X[回调 false<br/>停留锁定]
4. 账号验证机制(AccountVerifyHelper)
不是跳 Activity,是 ContentProvider call——base/settingsBaseLib/src/main/java/com/android/micar/settings/utils/AccountVerifyHelper.kt:
// AccountVerifyHelper.kt:143-149
contentResolver.call(Uri.parse("content://mi.car.account"), "confirm", null, bundle)| Bundle 参数 | 值 | 出处 |
|---|---|---|
car_confirm_remote_response | 匿名 IAccountConfirmResponse.Stub(Binder 回调) | :91-113 |
confirm_scene | "glovebox_verify"(手套箱专属场景号) | :70,127 |
confirm_style | "qr_code"(二维码;代码里也有 face_id 可选值 :59,但手套箱固定 qr_code :128) | :128 |
qr_tips / page_title | 扫码提示/标题文案 | :129-141 |
| 环节 | 内容 |
|---|---|
拉起结果 result_code | 0=成功拉起;1=未登录;2=验证中;3=未录入 face id;4=face id 超限;5=参数错误;6=不支持(:19-49)。仅 0 继续(:154) |
| 异步结果 | onResult(Bundle)(含 user_id)→ 主线程 mCallback(true);onError(code,msg)(取消 errCode=4)→ mCallback(false);回调一次性(置 null) |
| 取消 | call(uri,"cancelConfirm",...),在 GloveBoxPasswordDialog.showFragment() 前固定调用清残留(GloveBoxPasswordDialog.kt:74) |
调用分层:
flowchart LR G[GloveBoxAccountVerifyUtils<br/>+ P 挡检查] --> P[PinAccountVerifyUtils<br/>baseui 通用封装] P --> A[AccountVerifyHelper<br/>ContentProvider call] A --> SYS[(小米账号系统<br/>content://mi.car.account)]
P 挡检查(GloveBoxAccountVerifyUtils.kt:50-55):
fun openVerifyPage(isNeedCheckGear: Boolean = true, callback): Boolean {
if (isNeedCheckGear && getIntProperty(Driving.GEAR, DEFAULT_AREA_ID, Gear.PARK) != Gear.PARK) {
MiCarToastUtils.showToast(R.string.glove_box_fail_pwd_limit) // 「请驻车后重试」
return false
}
return PinAccountVerifyUtils.openVerifyPage(callback)
}| 场景 | isNeedCheckGear | 原因 |
|---|---|---|
| 开箱锁定→账号验证 | true(默认) | 驻车才允许解锁密码 |
| 设置弹窗内锁定→账号验证 | true(默认) | 同上 |
| 密码弹窗「忘记密码」 | false(BasePinPasswordFragment.kt:264) | 密码弹窗本身已在开箱流程中,重复挡位拦截会打断体验 |
5. 密码弹窗内部状态(BasePinPasswordFragment)
| 状态点 | 行为 | 出处 |
|---|---|---|
| 标题切换 | SET:「请设置密码」;CHECK:「请输入密码」;CHECK+close:「请输入密码以关闭」;CHANGE 旧密码:「请输入旧密码」→ 新密码:「请设置新密码」→ 再次:「请再次输入」 | BasePinPasswordFragment.kt:226-248,354-361 |
| 「忘记密码」可见性 | 仅 CHECK_PWD 与 CHANGE_PWD(旧密码阶段)显示;SET 与 CHANGE_PWD_VERIFIED 隐藏 | :209-251 |
| 指示器显示 | 最后一位明文数字,其余圆点;未输入空心 | :388-410 |
| 错误提示 | 密码错→pin_password_wrong;两次不一致→resDoubleCheckFailString;开始输入即隐藏 | :330-346,390 |
| 结果回传 | noticeVerifyResult() 发 LocalBroadcast ACTION_PASSWORD_VERIFY_RESULT + finish | :422-433 |
| 调用方收结果 | PinPasswordCheckUtils.mBroadcastReceiver 收到后回调 callback | PinPasswordCheckUtils.kt:64-70 |
⚠️ 文案归属:手套箱没有覆写
PinPasswordConfig的 string 资源(GloveBoxPasswordTools.kt:33-38只传了 4 个 key), 密码弹窗里实际显示的是 baseui 的pin_password_*通用文案,不是glove_box_pwd_*。 手套箱专属文案(glove_box_pwd_set_success / clean_success / change_success)只出现在设置弹窗的 Toast 上。
6. 代客/车主模式对业务流的影响
| 场景 | 行为 | 出处 |
|---|---|---|
| 代客模式点 Tab1(密码设置) | 置灰,Toast「请在用户模式下使用该功能」 | GloveBoxController.kt:134,181-186 |
| 代客模式点 Tab0(开箱) | 若属性不可用,Toast 兜底 glove_box_fail_valet_mode | GloveBoxController.kt:295 |
| 未登录账号 | Tab1 隐藏(02 篇 §1.3);开箱不受影响 | GloveBoxController.kt:86-96 |
| MIS 语音/远程路径 | 无代客/登录检查,仅靠密码与锁定拦截 | GloveBoxNavigationHandler.kt:62-86(05 篇) |
| 密码存储 | Settings.Global 全用户共享(AOSP 语义),非 per-user | PinPasswordTools.kt:266-330 |