手套箱 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_passwordSHA-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明文 intGloveBoxPasswordTools.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_code0=成功拉起;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 收到后回调 callbackPinPasswordCheckUtils.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_modeGloveBoxController.kt:295
未登录账号Tab1 隐藏(02 篇 §1.3);开箱不受影响GloveBoxController.kt:86-96
MIS 语音/远程路径无代客/登录检查,仅靠密码与锁定拦截GloveBoxNavigationHandler.kt:62-86(05 篇)
密码存储Settings.Global 全用户共享(AOSP 语义),非 per-userPinPasswordTools.kt:266-330