02 · 蓝牙配对(Pairing/Bond)全流程
范围:配对链路(扫描发现见 01,设备详情见 03) 核心文件:
MiBluetoothPairingController.kt、MiBluetoothPairingRequest.kt、view/MiBluetoothPairingDialogActivity.kt、controller/BluetoothOperationsController.java
1. 配对流程宏观
分发起端(车机主动 createBond)与响应端(对端主动,系统发 ACTION_PAIRING_REQUEST)两条路径,最终都汇聚到 MiBluetoothPairingDialogActivity。
关键调用点:
| 动作 | 调用点 |
|---|---|
| 点击可用设备发起 pair | BluetoothUnbondedDevicesPrefController.java:493 MiBluetoothUtils.pair(...) |
| 实际执行 | MiBluetoothUtils.kt:242(pair)→:259(innerPair)→:262(cachedDevice.startPairing()) |
| createBond 底层 | base/settingsLibAndroid/src/xcddif/.../CachedBluetoothDevice.java:613 mActiveDevice.createBond() |
| 接收系统配对请求 | MiBluetoothPairingRequest.kt:50(onReceive) · manifest:82-88 |
| 拉起配对弹窗 | MiBluetoothPairingRequest.kt:108 |
| 配对确认 | MiBluetoothPairingController.kt:242(onPair) |
| 配对取消 | MiBluetoothPairingController.kt:283(onCancel)→仅cancelBondProcess(⚠️2956未修复,缺bonded→removeBond降级,详见41) |
| 手柄自动确认 | MiBluetoothPairingRequest.kt:62-65(gamepad 跳过弹窗直接 onPair) |
sequenceDiagram autonumber actor U as 用户 participant UU as BluetoothUnbondedDevicesPrefController participant Utils as MiBluetoothUtils participant FW as 蓝牙框架 participant Req as MiBluetoothPairingRequest participant Act as MiBluetoothPairingDialogActivity participant Ctrl as MiBluetoothPairingController Note over U,FW: 路径A: 车机主动发起 U->>UU: 点击可用设备 UU->>Utils: pair(ctx, cachedDevice) (:493) Utils->>FW: startPairing→createBond (:262) FW-->>UU: bondState→BOND_BONDING Note over U,FW: 路径B: 对端主动 FW->>Req: ACTION_PAIRING_REQUEST Req->>Req: 设备类型过滤(:58)/手柄直通(:62)/双蓝牙上限拦截(:85) Req->>Act: startActivity(PairingDialogIntent) (:108) Note over Act,Ctrl: 弹窗与确认 Act->>Ctrl: MiBluetoothPairingController(intent) (:143) alt 需用户确认 U->>Ctrl: onPair() (:231) Ctrl->>FW: setPairingConfirmation(true)/setPin() (:254-265) else DISPLAY_PASSKEY 只展示 Ctrl->>FW: setPairingConfirmation(true) 自动 (:214) end FW-->>Act: ACTION_BOND_STATE_CHANGED (BOND_BONDED) Act->>Act: dismiss(isPairSuccess=true) (:55) alt isCarPlaySupportDevice && isCarPlaySupported Act->>Act: 延迟150ms 启动 MiCarplayConnectTypeChooseDialogActivity (:210) end
2. 配对状态机
DevicePairStageEvent 真相
DevicePairStageEvent.java:7-9 不是 stage 枚举,而是三元组状态值:
public boolean isBusy = false;
public boolean isConnected = false;
public int mBondState = BluetoothDevice.BOND_NONE;唯一使用方 BluetoothUnbondedDevicesPrefController.java:85-113(mPairingCallback),通过前后值对比判断”从未连接busy→已连接”这一刻发 sendBluetoothConnectedBroadcast(:103)。真正的”stage”就是蓝牙 bond state。
stateDiagram-v2 [*] --> BOND_NONE BOND_NONE --> BOND_BONDING : createBond() MiBluetoothUtils.kt:262 BOND_BONDING --> BOND_BONDED : 框架广播 BOND_BONDED BOND_BONDING --> BOND_NONE : cancelBondProcess (onCancel:283)br/或对端拒绝/超时br/⚠️对已BONDED无效(2956 需removeBond) BOND_BONDED --> BOND_NONE : unpair performUnpair(MiBluetoothExt.kt:55) state BOND_BONDING { [*] --> 展示配对弹窗 展示配对弹窗 --> 等待确认 : CONFIRMATION/USER_ENTRY 展示配对弹窗 --> 自动确认 : DISPLAY_PASSKEY/手柄 }
弹窗对 bondState 的响应(MiBluetoothPairingDialog.kt:105-139):BOND_BONDED/BOND_BONDING → 标题切换 + 按钮 GONE + loading VISIBLE;BOND_NONE → 保持原配置。
⚠️ 广播延迟窗口(MANNPROB-2956 实证修正)
ACTION_BOND_STATE_CHANGED 的 BONDED 广播不是协议栈 BONDED 那一刻就发——com.android.bluetooth 的 BondStateMachine 会先做 SDP/GATT service discovery 把对端 profile UUID 拉回,等 SDP 完成再发 BONDED 广播(AOSP BondStateMachine.java:583 “is bonded, wait for SDP complete”)。2956 实测:协议栈 44.860 已 BONDED,应用层广播 46.534 才发,延迟 1.674s。
sequenceDiagram participant Stack as 协议栈 participant BSM as BondStateMachine participant App as 配对弹窗receiver Stack->>BSM: 底层 BOND_BONDED (44.860) BSM->>BSM: wait for SDP UUIDs 推迟广播 (44.862) Note over App: 此窗口内 getBondState()已=BONDED<br/>但广播未到 弹窗仍按BONDING处理 Note over BSM: 1.674s 后 BSM-->>App: BONDED 广播 (46.534 迟到) App->>App: dismiss 走配对成功分支
含义:①”配对完成弹窗立刻 dismiss”在应用层无法可靠实现(最早 bonded 信号就是被延迟的广播);②做决策用 getBondState()(读服务侧实时态),广播只用于 UI 刷新;③receiver 取消后必须即时解注册——2956 缺陷二就是 receiver 到 onDestroy 才解注,迟到广播仍拉起 CarPlay 弹窗。详见 41-经典案例、20-框架全景 §2。
3. 三类对话框辨析(对抗核实纠正)
⚠️ 纠正:配对调研曾把
BluetoothDisconnectConfirmDialogFragment列为”四类对话框之一”。对抗核实(全仓库 grep)证实该类仅类内自引用、未被 MiCar 实例化,是死代码。实际断开确认走AlertDialog.MiCarBuilder内联(BluetoothBondedDevicesPrefController.java:220-243)。故此处为三类。
| 对话框 | Activity/Fragment | 触发场景 | 核心区别 |
|---|---|---|---|
| PairingDialog | MiBluetoothPairingDialogActivity+Fragment+Dialog | 系统 ACTION_PAIRING_REQUEST 或车机 createBond 后框架回广播 | 响应配对请求:展示 passkey/PIN、用户确认或输入;成功后转 loading 等连接 |
| PickerDialog | MiBluetoothPickerDialogActivity+Fragment | 已连接数 ≥ DOUBLE_BLUETOOTH_DEVICE_LIMIT=2(ConnectionConstants.kt:19) 时再配对/连接 | 选择被替换设备(双蓝牙冲替):选 A 则断 A 再连新设备;可携带 pendingPairRequestIntent 选完继续配对(:169) |
| CarplayConnectTypeChoose | MiCarplayConnectTypeChooseDialogActivity+Fragment | CarPlay 设备配对成功后(BOND_BONDED) | 选蓝牙连接还是 CarPlay:选 CarPlay 走 deeplink connection://carplay&bt_mac= |
关键约束:三个独立 Activity 都用 singleTask + 相同 taskAffinity="car.settings.bluetooth"(manifest:43,56,69),会落在同一任务栈互相顶替——后启动的顶替前一个,故 CarPlay 弹窗启动需 150ms 延迟等配对弹窗动画结束。
4. MiBluetoothPairingController / Request 深度
MiBluetoothPairingController(:17)
职责:单一配对意图的”翻译器”,把框架 PAIRING_VARIANT_* 翻译成”弹什么框 + 确认后调什么 API”。无生命周期,纯值对象,由 MiBluetoothPairingDialogActivity.doPairDevice():143 构造注入 Fragment。
对话框类型判定(:94-114 getDialogType):
USER_ENTRY_DIALOG(0):PIN/PIN_16/PASSKEY → 需用户输入CONFIRMATION_DIALOG(1):PASSKEY_CONFIRMATION/CONSENT/OOB_CONSENT → 仅确认DISPLAY_PASSKEY_DIALOG(2):DISPLAY_PASSKEY/DISPLAY_PIN → 只读INVALID_DIALOG_TYPE(-1):未知不弹
关键操作:onPair():242 按 variant 调 setPin/setPairingConfirmation(true),同时发 sendConnectNewDeviceBroadcast 通知音乐 App 暂停;onCancel():283 仅调 cancelBondProcess(⚠️2956:对已BONDED状态空转 Res:11,缺 bonded→removeBond 降级,详见41);notifyDialogDisplayed():210 DISPLAY_PASSKEY 时自动确认。
本应用未用系统
android.bluetooth.BluetoothPairingRequest(AOSP 实现),自建MiBluetoothPairingRequest拉自己的 DialogActivity。
MiBluetoothPairingRequest(:17,BroadcastReceiver,manifest:82-88)
ACTION_PAIRING_REQUEST 统一入口,三层过滤分流:
- 设备类型过滤(:58)
isDeviceClassTypeSupport→ 不支持直接丢 - 手柄直通(:62-65)
isGamePadDevice→ 不弹窗,直接onPair()自动确认 - 双蓝牙上限拦截(:78-102):已达 2 且需冲替 → 先弹 Picker,选完继续配对(携带 pairingIntent,在
MiBluetoothPickerDialogActivity.dismiss():169取出) - CarPlay 设备 mute 自动连接(:103-107):
isCarPlaySupportDevice && isCarPlaySupported→disableAutoConnect(写Settings.Secure BLUETOOTH_CLOSE_AUTO_CONNECT),避免配对后自动连蓝牙抢占 CarPlay - 默认(:108):
startActivityAsUser(getPairingDialogIntent)
5. BluetoothOperationsController 编排(:30)
不负责配对,负责已配对设备的 connect/disconnect/unpair,挂在设备详情页。唯一分派点 handlePreferenceChanged:144:
| PreferenceAction | 处理 |
|---|---|
Connect :148 | 先 confirmConnectWhenMisExist(复合连接二次确认)→ connectDevice():115;已连 < 2 直连,否则弹 Picker 冲替 :135 |
DisConnect :160 | confirmDisconnectWhenMisConnected → performDisConnect:164 断开后 goBack |
Unpair :179 | confirmDeleteDeviceWhenMisExist 或 showDialog(DIALOG_DEVICE_UNPAIR):197 → performUnpair,记 setLastUnboundDevice 拦截重新出现 :187 |
不维护操作队列:事件驱动,fire-and-forget,依赖底层 CachedBluetoothDevice 串行化。busy 联动:注册 CachedBluetoothDevice.Callback(:35)updateConnectionButtonState:80,BOND_NONE→goBack(:84-89),按钮可用性=!isBusy()(:91)。生命周期绑 Callback:onStartInternal 注册/onStopInternal 解注/setCachedDevice 切换设备先解旧再注新。
底层三件套(MiBluetoothExt.kt):performConnect:16、performDisConnect:32、performUnpair:55。
边界:配对(pair/createBond)由
MiBluetoothUtils.pair直接走,不经 BluetoothOperationsController;后者只在 BONDED 后介入。
6. CarPlay 二次弹窗
为什么:支持 CarPlay 的苹果设备同时具备蓝牙 HFP/A2DP profile 和 CarPlay 投屏能力。配对只建立链路层信任,“用什么协议跑业务”需用户选。不弹窗则系统默认 auto-connect 直接连蓝牙,无法进 CarPlay。
判定(两层):
- 车机支持 CarPlay(ROM 静态):
ConnectionUtils.isCarPlaySupported():117 - 被配对设备支持 CarPlay(运行时 UUID):
isCarPlaySupportDevice(device):101→ EIR UUID 含2d8d2466-... - 两者都真,且非 MIS 自发起(
!isBluetoothBondingByMe),在MiBluetoothPairingDialogActivity.dismiss(isPairSuccess=true):197触发
链路:
- 配对前(
MiBluetoothPairingRequest.kt:103-107):disableAutoConnect - 配对中(
:133-137):setCanExecuteScan(false,"pairing_carplay_device")阻断扫描 - 配对成功(
:197-218):dismiss 延迟 150ms(CARPLAY_CONNECT_DIALOG_START_DELAY_MS)启动类型选择 Activity - 类型选择 onCreate(
:52-54):延迟 200ms(SCAN_RESUME_DELAY_MS)setCanExecuteScan(true,"connect_type_choose_inflated")恢复扫描
两分支:选蓝牙 connection_bt(:95-118)→ clearDisableAutoConnect → 未达上限 performConnect,否则弹 Picker;选 CarPlay connection_carplay(:121-127)→ ComplexConstant.setLastConnectTypeCarplay → requestConnectedCarplay(btMac):137 发 deeplink connection://carplay&bt_mac=。
7. 设计要点与坑
- 无应用层配对超时:依赖系统栈
cancelBondProcess超时,监听ACTION_BOND_STATE_CHANGED回 BOND_NONE/ERROR 时 dismiss(MiBluetoothPairingDialogActivity.kt:51-57,ERROR 按失败处理)。 - 重复点击防护(三层):Controller 层
mIsHandlePair(Unbonded:456-460);对话框按钮点确认后updateBondState(BOND_BONDING)把按钮 GONE;详情页 Preference 点击setEnabled(false)。 - 配对≠连接:createBond 后 BOND_BONDED,但 HFP/A2DP 连接是异步的。
MiBluetoothPairingDialogBOND_BONDED 后切 loading(:112-122),等 ACL_DISCONNECTED 或 PAIRING_CANCEL 才关闭——配对成功后弹窗挂着直到连接完成或断开。无 ACL_CONNECTED 监听。 cancelPairBySelf标志位(MiBluetoothUtils.kt:50):用户主动取消设 true,避免弹错误 Toast。坑:全局静态变量,两配对弹窗并发(理论上 singleTask 顶替)会互相覆盖。- taskAffinity 联动:三对话框 Activity 共用
car.settings.bluetooth+singleTask会互相顶替,故 CarPlay 弹窗需 150ms 延迟;MiBluetoothPickerDialogActivity.onPause:150直接 safeFinish 避免共存。 ACTION_CLOSE_ACTIVITY(BluetoothSettingsActivity.kt:26):com.micar.settings.action.CLOSE_BLUETOOTH_SETTINGS_ACTIVITY,CarPlay/AA 连接后打开自家弹窗时发此广播自动关蓝牙列表页。- 双蓝牙上限=2 三处拦截:
MiBluetoothPairingRequest.kt:85(对端发起)、BluetoothUnbondedDevicesPrefController.java:482(车机发起,仅对讲机)、BluetoothOperationsController.java:125/MiCarplayConnectTypeChooseDialogFragment.kt:106(已配对主动连接)。 isPairingAsClient(MiBluetoothExt.kt:68):区分主动/被动配对,影响弹窗标题文案(as_client/as_server 两套)与主动配对时隐藏确认按钮直接执行。