41 · 经典案例 · MANNPROB-2956(配对取消仍成功)
素材:
/home/zbc/下载/test/车机JIRA/jira-data/MANNPROB-2956/(root-cause.md + 15 条 bugreport 证据 + 10 处 file:line + 11 帧视频) 适用范围:排障方法论教学——展示”想当然的根因如何被三层证据链推翻” 关联:11-配对与SSP、20-框架全景、31-连接策略
⚠️ 当前状态(2026-07-23 二次核实):修复 commit
7239803be在dev_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.698 | 195848 | App | CarSettings createBond |
| 16:24:40.701 | 195914 | 服务 | BOND_NONE⇒BONDING 广播即时发出(仅 3ms) |
| 16:24:43.084 | 205431 | 协议栈 | is_auto_accept_ssp=false(决定权上抛 App) |
| 16:24:43.086 | 205453 | 服务 | PAIRING_REQUEST variant=2 passkey=196742 |
| 16:24:43.184 | 205973 | App | 弹窗创建后 2ms 自动 onPair→setPairingConfirmation(true) |
| 16:24:44.855 | 213311/213317 | 协议栈 | Storing link key key_type=0x8(BR/EDR Link Key 落盘) |
| 16:24:44.860 | 213396 | 协议栈 | BOND_BONDED(应用层广播未发) |
| 16:24:44.862 | 213446 | 服务 | wait for service discovery UUIDs(BondStateMachine 推迟广播) |
| 16:24:45.087 | 214289-214323 | 协议栈 | CTKD:SMP_BR 从 Link Key 派生 LE LTK + 存 IRK(BTM_LE_KEY_PID/214291)/LE LTK(214318) |
| 16:24:46.336 | 221952 | App | 用户点取消 |
| 16:24:46.339 | 221988 | 协议栈 | btm_confirm_req_reply State: IDLE Res: 11(取消空转) |
| 16:24:46.534 | 224044 | 服务 | 迟到的 BONDED 广播(取消后 198ms) |
| 16:24:46.552 | 224281 | App | dismiss open connect type choose dialog(CarPlay 弹窗) |
| 16:24:46.583 | 224598 | 协议栈 | 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(栈层内部值)与行 205453sendPairingRequestIntent 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-182:isRequestPairAsClient=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
7239803be在dev_a17_0610为悬空提交(git merge-base --is-ancestor返回非祖先、git fsckdangling),onCancel 仍旧版,bug 仍在。 - 叠加 H6(f3dif/A17):f3dif
CachedBluetoothDevice无isCloseAutoConnectDevice,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. 验证方法
- 修复后回归:车机发起配对→手机确认→车机点取消,验证 logcat 出现
removeBond、BOND_BONDED⇒BOND_NONE,无 HFP/A2DP 连接、无 CarPlay 弹窗、无密钥残留。 - 延迟点击/双适配器回归。
- A17 额外验证 CarPlay 配对后是否仍自动连(f3dif H6)。
9. 方法论小结
- 建三层时间线:App/服务/协议栈对齐,找时序错位(44.862 广播延迟是本案钥匙)。
- 证伪而非证实:H4 被”全程仅一次广播”一票推翻;H1 被”receiver 正常”证伪机制。
- getBondState 才权威:广播有延迟,决策读服务侧实时态。
- 区分”协议要求”与”App 决策”:自动 accept 是 App 绕过,非协议强制(
is_auto_accept_ssp=false为证)。 - 核实修复是否真合入:
git merge-base --is-ancestor,勿信git branch --contains(受本地状态影响)——本案修复 commit 悬空,差点被当成”已修复”。 - 跨 flavor 核实:A17(f3dif) 黑名单失效是独立叠加问题,单看 xcddif 行号会漏。