41 · 经典案例 · MANNPROB-2956(配对取消仍成功)

素材:/home/zbc/下载/test/车机JIRA/jira-data/MANNPROB-2956/(root-cause.md + 15 条 bugreport 证据 + 10 处 file:line + 11 帧视频) 适用范围:排障方法论教学——展示”想当然的根因如何被三层证据链推翻” 关联:11-配对与SSP20-框架全景31-连接策略

⚠️ 当前状态(2026-07-23 二次核实):修复 commit 7239803bedev_a17_0610悬空提交(已被 reset),未合入,bug 仍在。本文是”根因已定位、修复待合入”的教学案例。

1. 问题现象

  • 复现:车机发起配对 iPhone → 手机点配对 → 车机弹确认框(配对码 196742,仅”取消”钮)→ 用户点取消 → 仍配对成功,自动连 HFP/A2DP,还弹”已完成配对”CarPlay 选择框
  • 预期:取消后不配对成功
  • 概率:必现(Always)
  • 视频逐帧:f_4s 车机弹确认+手机弹请求;f_6s 手移向”取消”时手机已显示已配对;f_7s 车机”正在连接”;f_10s 车机弹”已完成配对”

2. 设备环境

Xiaomi fermi(MS13_Modena-L)/ Android 17(API 37) / xring 玄戒 O3 / CP2A.260605.016;对端 iPhone(CarPlay 设备)。

3. 三层时间线重建(bugreport 主 txt 行号)

方法论:排障第一步是建”App 层 ↔ 服务层(BondStateMachine) ↔ 协议栈(bt_*)“三层对齐时间线,找”谁先谁后”。本案例铁证在 44.862 那行——它解释了”广播延迟”,也推翻了最初的想当然。

时间行号事件
16:24:40.698195848AppCarSettings createBond
16:24:40.701195914服务BOND_NONE⇒BONDING 广播即时发出(仅 3ms)
16:24:43.084205431协议栈is_auto_accept_ssp=false(决定权上抛 App)
16:24:43.086205453服务PAIRING_REQUEST variant=2 passkey=196742
16:24:43.184205973App弹窗创建后 2ms 自动 onPairsetPairingConfirmation(true)
16:24:44.855213311/213317协议栈Storing link key key_type=0x8(BR/EDR Link Key 落盘)
16:24:44.860213396协议栈BOND_BONDED(应用层广播未发
16:24:44.862213446服务wait for service discovery UUIDs(BondStateMachine 推迟广播)
16:24:45.087214289-214323协议栈CTKD:SMP_BR 从 Link Key 派生 LE LTK + 存 IRK(BTM_LE_KEY_PID/214291)/LE LTK(214318)
16:24:46.336221952App用户点取消
16:24:46.339221988协议栈btm_confirm_req_reply State: IDLE Res: 11取消空转
16:24:46.534224044服务迟到的 BONDED 广播(取消后 198ms)
16:24:46.552224281Appdismiss open connect type choose dialog(CarPlay 弹窗)
16:24:46.583224598协议栈HeadsetClient + A2dpSink 自动连接
sequenceDiagram
    participant App as CarSettings
    participant BSM as BondStateMachine(服务)
    participant Stack as bluedroid(协议栈)
    App->>Stack: createBond (40.698)
    Stack-->>App: BONDING 广播即时 (40.701, 3ms)
    Stack->>Stack: is_auto_accept_ssp=false (43.084)
    BSM-->>App: PAIRING_REQUEST variant=2 (43.086)
    App->>Stack: 自动 setPairingConfirmation(true) (43.184, 2ms)
    Stack->>Stack: Storing link key key_type=8 (44.855)
    Stack->>Stack: BOND_BONDED (44.860 应用层未广播)
    BSM->>BSM: wait for SDP UUIDs 推迟广播 (44.862)
    Stack->>Stack: CTKD 派生 LTK+存IRK (45.087)
    App->>Stack: 用户取消 onCancel (46.336)
    Stack-->>App: State:IDLE Res:11 空转 (46.339)
    Note over BSM: 1.674s 后
    BSM-->>App: 迟到 BONDED 广播 (46.534)
    App->>App: dismiss→开CarPlay弹窗 (46.552)
    Stack-->>Stack: HFP+A2dp自动连 (46.583)

variant 命名鸿沟:43.086 行 205446 sspRequestCallback pairingVariant 0(栈层内部值)与行 205453 sendPairingRequestIntent variant=2(app 层 PAIRING_VARIANT_PASSKEY_CONFIRMATION)同毫秒双行——经 frameworks 翻译,BR/EDR SC 下都对应 Numeric Comparison UI,勿误以为日志矛盾。详见 11 §3。

两个常被误读的时间点:① 44.855 才是 BR/EDR Link Key 落盘(不是 45.087);② 45.087 是 CTKD 从 Link Key 派生 LE LTK + 存 IRK(BR→LE 方向,非”LE 独立配对”)。

4. 四个假设与对抗推翻(方法论核心)

方法论:每个假设都要用一手证据去证伪,而不是找证据支持。能被证伪的假设才排除;证据闭合、无存活反例的才定为主因。

假设质疑裁决
H1:确认框未随 bonded 消失(最初判断)源码反驳:receiver 注册正常(广播一到 46.552 就 dismiss);“立刻释放”依赖的广播本身被框架延迟 1.67s,应用层无法消除窗口备选——正确现象 + 错误机制解释
H2:onCancel 无已 bonded 降级”AOSP 也一样只有 cancelBondProcess”→反事实强化:AOSP 无自动 accept 故竞态窗口不存在,小米组合使其必现;46.336 时 getBondState() 已 BONDED,if(bonded) removeBond() 可命中主因(代码单行 + IDLE Res=11 + removeBond=0次 + LTK落盘,无存活反例)
H3:自动 accept 无否决窗口”SSP Just Works 协议要求”辩护不成立:variant=2 是 Numeric Comparison,协议为双向确认设计,is_auto_accept_ssp=false 为证使能条件/设计争议——反事实:只修 H2 保留自动 accept,用户预期仍可达成
H4:框架取消后重复广播 BONDED被一手证据推翻:全程仅一次 BONDED 广播(44.862 被推迟的那次),非重放排除(原表述自灭);其试图解释的现象真实存在,根因在 App receiver 生命周期,并入缺陷二

5. 最终根因(两个独立 App 缺陷 + 一个设计争议 + 一个框架诱因)

主因:onCancel 无 removeBond 降级

MiBluetoothPairingController.kt:283 onCancel当前仍是旧版):

fun onCancel() {
    MLog.tag(TAG).d("onCancel")
    mDevice.cancelBondProcess()   // 仅此一行,无 bonded 状态判断
}

cancelBondProcess 只对进行中(BOND_BONDING)有效;用户 46.336 点取消时,协议栈 44.855 早已落 Link Key、BONDED——落入 IDLE 态必然空转(Res=11),配对必然落定。全 bugreport removeBond 出现 0 次

缺陷二:receiver 未解注册,迟到广播仍走”配对成功”路径

MiBluetoothPairingDialogActivity.kt 的 broadcast receiver 到 onDestroy(46.966) 才解注。用户 46.339 已 dismiss,但 46.534 迟到的 BONDED 广播仍被处理,dismiss(isPairSuccess=true) 在 Activity 已 finishing 时 postDelayed 拉起 CarPlay 选择弹窗(46.552,与视频 f_10s 互证)。

使能条件(设计争议):自动 accept 消灭否决窗口

MiBluetoothPairingDialogFragment.kt:179-182isRequestPairAsClient=true(车机主动发起)时弹窗创建瞬间即 onPair()(43.184→43.186 仅 2ms)。注释”主动配对不展示确认button,直接执行车机侧确认动作”。使”取消必然落后于配对完成”成为必然。注意:variant=2 是 Numeric Comparison,协议层需确认(is_auto_accept_ssp=false 为证),是 App 主动绕过,非协议要求

框架诱因(非 bug):BONDED 广播被 SDP 延迟 1.674s

44.862 wait for service discovery UUIDs——BondStateMachine 等 SDP 完成再发 BONDED 广播(详见 20 §2)。制造”原生已 bonded、应用层仍 BONDING”的竞态窗口,用户取消恰好落入。闫浩提的”配对完成 pair dialog 立刻释放”在应用层无法可靠实现

关键认知getBondState() 读协议栈服务侧实时态(46.336 已 BONDED),广播是被延迟的副本——做决策用 getBondState(),不要用广播到达当时间锚点。这是修复方案能成立的前提。

6. ⚠️ 当前未修复 + f3dif 叠加风险

  • 未修复:commit 7239803bedev_a17_0610 为悬空提交(git merge-base --is-ancestor 返回非祖先、git fsck dangling),onCancel 仍旧版,bug 仍在。
  • 叠加 H6(f3dif/A17):f3dif CachedBluetoothDeviceisCloseAutoConnectDevice,disableAutoConnect 黑名单失效——即便配对取消逻辑修好,A17 上 CarPlay 设备配对后仍会自动连 HFP/A2DP 抢占通道(见 31 H6)。

7. 修复方案(待合入)

改动1(主修复,满足”取消不配对成功”)onCancel 加 bonded 降级

fun onCancel() {
    if (mDevice.bondState == BluetoothDevice.BOND_BONDED) {
        mDevice.removeBond()   // 等 BOND_NONE 广播后再视为取消完成
    } else {
        mDevice.cancelBondProcess()
    }
}

改动2(必须配套,否则 CarPlay 弹窗残留)MiBluetoothPairingDialogActivity.kt 取消 dismiss 时立即解注册 receiver(或 mDismissed 标志,onReceive 直接 return);dismiss(isPairSuccess) 的 CarPlay 拉起分支移到 isFinishing 检查之后。

改动3(产品裁决项):若产品要求车机发起配对也需用户车机侧确认,则将 Fragment:179-182 自动 onPair 改为等用户点击(回归 AOSP 语义)。

待查com.android.bluetooth BondStateMachine 在”wait for service discovery UUIDs”期间,cancelBondProcess 是否应清除 pending BONDED 广播(若框架不清除,改动2必不可少)。

8. 验证方法

  1. 修复后回归:车机发起配对→手机确认→车机点取消,验证 logcat 出现 removeBondBOND_BONDED⇒BOND_NONE,无 HFP/A2DP 连接、无 CarPlay 弹窗、无密钥残留。
  2. 延迟点击/双适配器回归。
  3. A17 额外验证 CarPlay 配对后是否仍自动连(f3dif H6)。

9. 方法论小结

  1. 建三层时间线:App/服务/协议栈对齐,找时序错位(44.862 广播延迟是本案钥匙)。
  2. 证伪而非证实:H4 被”全程仅一次广播”一票推翻;H1 被”receiver 正常”证伪机制。
  3. getBondState 才权威:广播有延迟,决策读服务侧实时态。
  4. 区分”协议要求”与”App 决策”:自动 accept 是 App 绕过,非协议强制(is_auto_accept_ssp=false 为证)。
  5. 核实修复是否真合入git merge-base --is-ancestor,勿信 git branch --contains(受本地状态影响)——本案修复 commit 悬空,差点被当成”已修复”。
  6. 跨 flavor 核实:A17(f3dif) 黑名单失效是独立叠加问题,单看 xcddif 行号会漏。