02 · 蓝牙配对(Pairing/Bond)全流程

范围:配对链路(扫描发现见 01,设备详情见 03) 核心文件:MiBluetoothPairingController.ktMiBluetoothPairingRequest.ktview/MiBluetoothPairingDialogActivity.ktcontroller/BluetoothOperationsController.java

1. 配对流程宏观

发起端(车机主动 createBond)与响应端(对端主动,系统发 ACTION_PAIRING_REQUEST)两条路径,最终都汇聚到 MiBluetoothPairingDialogActivity

关键调用点

动作调用点
点击可用设备发起 pairBluetoothUnbondedDevicesPrefController.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-113mPairingCallback),通过前后值对比判断”从未连接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.bluetoothBondStateMachine 会先做 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触发场景核心区别
PairingDialogMiBluetoothPairingDialogActivity+Fragment+Dialog系统 ACTION_PAIRING_REQUEST 或车机 createBond 后框架回广播响应配对请求:展示 passkey/PIN、用户确认或输入;成功后转 loading 等连接
PickerDialogMiBluetoothPickerDialogActivity+Fragment已连接数 ≥ DOUBLE_BLUETOOTH_DEVICE_LIMIT=2(ConnectionConstants.kt:19) 时再配对/连接选择被替换设备(双蓝牙冲替):选 A 则断 A 再连新设备;可携带 pendingPairRequestIntent 选完继续配对(:169)
CarplayConnectTypeChooseMiCarplayConnectTypeChooseDialogActivity+FragmentCarPlay 设备配对成功后(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 统一入口,三层过滤分流:

  1. 设备类型过滤(:58)isDeviceClassTypeSupport → 不支持直接丢
  2. 手柄直通(:62-65)isGamePadDevice → 不弹窗,直接 onPair() 自动确认
  3. 双蓝牙上限拦截(:78-102):已达 2 且需冲替 → 先弹 Picker,选完继续配对(携带 pairingIntent,在 MiBluetoothPickerDialogActivity.dismiss():169 取出)
  4. CarPlay 设备 mute 自动连接(:103-107):isCarPlaySupportDevice && isCarPlaySupporteddisableAutoConnect(写 Settings.Secure BLUETOOTH_CLOSE_AUTO_CONNECT),避免配对后自动连蓝牙抢占 CarPlay
  5. 默认(:108):startActivityAsUser(getPairingDialogIntent)

5. BluetoothOperationsController 编排(:30)

不负责配对,负责已配对设备的 connect/disconnect/unpair,挂在设备详情页。唯一分派点 handlePreferenceChanged:144

PreferenceAction处理
Connect :148confirmConnectWhenMisExist(复合连接二次确认)→ connectDevice():115;已连 < 2 直连,否则弹 Picker 冲替 :135
DisConnect :160confirmDisconnectWhenMisConnectedperformDisConnect:164 断开后 goBack
Unpair :179confirmDeleteDeviceWhenMisExistshowDialog(DIALOG_DEVICE_UNPAIR):197performUnpair,记 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:16performDisConnect:32performUnpair: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 触发

链路

  1. 配对前(MiBluetoothPairingRequest.kt:103-107):disableAutoConnect
  2. 配对中(:133-137):setCanExecuteScan(false,"pairing_carplay_device") 阻断扫描
  3. 配对成功(:197-218):dismiss 延迟 150msCARPLAY_CONNECT_DIALOG_START_DELAY_MS)启动类型选择 Activity
  4. 类型选择 onCreate(:52-54):延迟 200msSCAN_RESUME_DELAY_MSsetCanExecuteScan(true,"connect_type_choose_inflated") 恢复扫描

两分支:选蓝牙 connection_bt(:95-118)→ clearDisableAutoConnect → 未达上限 performConnect,否则弹 Picker;选 CarPlay connection_carplay(:121-127)→ ComplexConstant.setLastConnectTypeCarplayrequestConnectedCarplay(btMac):137 发 deeplink connection://carplay&bt_mac=

7. 设计要点与坑

  1. 无应用层配对超时:依赖系统栈 cancelBondProcess 超时,监听 ACTION_BOND_STATE_CHANGED 回 BOND_NONE/ERROR 时 dismiss(MiBluetoothPairingDialogActivity.kt:51-57,ERROR 按失败处理)。
  2. 重复点击防护(三层):Controller 层 mIsHandlePair(Unbonded:456-460);对话框按钮点确认后 updateBondState(BOND_BONDING) 把按钮 GONE;详情页 Preference 点击 setEnabled(false)
  3. 配对≠连接:createBond 后 BOND_BONDED,但 HFP/A2DP 连接是异步的。MiBluetoothPairingDialog BOND_BONDED 后切 loading(:112-122),等 ACL_DISCONNECTED 或 PAIRING_CANCEL 才关闭——配对成功后弹窗挂着直到连接完成或断开。无 ACL_CONNECTED 监听。
  4. cancelPairBySelf 标志位MiBluetoothUtils.kt:50):用户主动取消设 true,避免弹错误 Toast。:全局静态变量,两配对弹窗并发(理论上 singleTask 顶替)会互相覆盖。
  5. taskAffinity 联动:三对话框 Activity 共用 car.settings.bluetooth+singleTask 会互相顶替,故 CarPlay 弹窗需 150ms 延迟;MiBluetoothPickerDialogActivity.onPause:150 直接 safeFinish 避免共存。
  6. ACTION_CLOSE_ACTIVITYBluetoothSettingsActivity.kt:26):com.micar.settings.action.CLOSE_BLUETOOTH_SETTINGS_ACTIVITY,CarPlay/AA 连接后打开自家弹窗时发此广播自动关蓝牙列表页。
  7. 双蓝牙上限=2 三处拦截MiBluetoothPairingRequest.kt:85(对端发起)、BluetoothUnbondedDevicesPrefController.java:482(车机发起,仅对讲机)、BluetoothOperationsController.java:125/MiCarplayConnectTypeChooseDialogFragment.kt:106(已配对主动连接)。
  8. isPairingAsClientMiBluetoothExt.kt:68):区分主动/被动配对,影响弹窗标题文案(as_client/as_server 两套)与主动配对时隐藏确认按钮直接执行。