32 · 双蓝牙上限与 Picker 冲替机制

核心文件:ConnectionConstants.ktMiBluetoothPickerDialogActivity.ktMiBluetoothPickerDialogFragment.ktDevicePairStageEvent.java 适用范围:双蓝牙上限与设备选择(冲替/Picker 拉起/配对意图接力) 关联:02-配对流程31-连接策略

1. 上限常量与判定口径

ConnectionConstants.kt:19 const val DOUBLE_BLUETOOTH_DEVICE_LIMIT = 2。“已达上限”判定口径统一为已连接 HFP/A2DP 设备数ConnectionUtils.getConnectedHfpA2dpDevices :406/413),非已配对数。非手机类(无 HFP/A2DP,如对讲机、CarPlay-only)不计入——所以”2手机+1耳机”可共存。

2. 拦截点:五处调用点、三类场景(行号已区分 if-check vs call)

对抗修正:02 文档列”三处拦截”按场景归纳对,但第三类”主动连接”含 3 个调用点,且行号需区分”判定行(if)“与”调用行(call)“。下表用调用行displayBluetoothDevicePicker(...) 实际所在行)。

#场景文件判定行(if)调用行(call)拦截后动作
1对端发起配对MiBluetoothPairingRequest.kt:85:90弹 Picker,携 pairingIntent,选完 dismiss 接力继续配对
2车机发起配对(仅对讲机)BluetoothUnbondedDevicesPrefController.java:524:526弹 Picker,autoDisconnect=true/autoConnect=false,不传 intent
3a详情页主动连接BluetoothOperationsController.java:132:142弹 Picker,断旧+500ms后 performConnect 新
3b主页已配对点连接BluetoothBondedDevicesPrefController.java:273:295同 3a(02 文档原遗漏此入口)
3cCarPlay 配对后选蓝牙MiCarplayConnectTypeChooseDialogFragment.kt:106:111同 3a

对讲机识别 = MAC 前缀 CC:72:86(MiBluetoothUtils.kt:37 RADIO_DEVICE_ADDRESS_PREFIXisTwoWayRadio :378-383)。故第2处只对对讲机生效——普通手机在可用列表点击即使满2个也走 MiBluetoothUtils.pair(:535),由对端 MiBluetoothPairingRequest 兜底拦截。

flowchart TD
    A[蓝牙操作触发] --> B{类型?}
    B -->|对端配对| C[PairingRequest:90 查hfpA2dp≥2]
    B -->|车机点可用设备| D[Unbonded:519 是否对讲机?]
    B -->|已配对主动连接| E[三路径: Ops:142/Bonded:295/CarPlay:111]
    C -->|达上限| P[(PickerDialog<br/>携pairingIntent)]
    D -->|对讲机且达上限| P
    E -->|达上限且isValidTriggerDevicePicker| P
    P --> Q{选被替换A/B}
    Q -->|选A| R[断A→按场景连新/接力配对]

3. MiBluetoothPickerDialog 冲替机制

MiBluetoothPickerDialogActivity(:24) 是壳,MiBluetoothPickerDialogFragment(:44) 是实际 CarUiDialogFragment。两个 displayBluetoothDevicePicker 重载(:45 四参 / :53 六参多 pairingIntent+isFromReceive)。

autoConnect+DEVICE_PAIR_INTENT 两 extra 决定语义

调用方autoConnectDEVICE_PAIR_INTENT语义
PairingRequest:90false非空选A→断A→dismiss 接力启动配对弹窗(不连新设备,因未配对)
其余4处(:526/:142/:295/:111)truenull选A→断A→延时500ms performConnect新设备

冲替执行(MiBluetoothPickerDialogFragment.onClick:175-215):点 device_a/b(最多2个 :157-168)→disConnectChooseDevice(:223,performDisConnect(true) 断旧,同步)→doConnect(:238)。autoConnect=true 分支(:241-254) mHandler.postDelayed(ConnectRunnable, 500)(:252) 延时连新(500ms 等断开完成,避免再次触发冲替)。

Picker 是配对弹窗的前置闸门(非子组件):仅 MiBluetoothPairingRequest 路径拉起 Picker 时把原始配对 intent 寄存;其余4处传 null,dismiss 时空跳过。

4. DevicePairStageEvent 状态传递

DevicePairStageEvent.java:7-9 不是 stage 枚举,是三元组瞬时状态值:

public boolean isBusy = false;
public boolean isConnected = false;
public int mBondState = BOND_NONE;

事件源 = mPairingCachedDevice(CachedBluetoothDevice 实例)状态变化触发的 onDeviceAttributesChanged 回调;callback 注册方与消费方都在 BluetoothUnbondedDevicesPrefControllermPairingCallback :85-114,挂在 mPairingCachedDevice :79)。注册:用户点可用设备时(:511/515);解注:切设备(:513)/停止(:237)。不跨类传递,是 Controller 内部”前一次状态快照”。

边沿检测:配对/连接中 onDeviceAttributesChanged(:87) 反复触发,需识别”从未连接 busy→已连接”那一刻只发一次 sendBluetoothConnectedBroadcast(:104)。机制是前后值对比(:101-105):

if ((prev.isBusy != cur.isBusy || prev.isConnected != cur.isConnected)
    && !cur.isBusy && cur.isConnected) {
    MiBluetoothUtils.sendBluetoothConnectedBroadcast(address);  // :104
}

即”前一次 busy/connected 至少一个变化 AND 本次非busy且已连”→边沿,发广播。随后 cur 覆写 prev(:109-111)作下帧基线。无此机制则要么每帧发(噪声)要么漏发。

stateDiagram-v2
    direction LR
    [*] --> Prev_None: mPairStageEvent 初始null
    Prev_None --> Compare: onDeviceAttributesChanged 每帧 prev vs cur
    Compare --> FireBroadcast: prev(isBusy|isConn)≠cur AND !cur.isBusy AND cur.isConnected
    Compare --> Skip: 否则
    FireBroadcast --> UpdatePrev: cur覆写prev
    UpdatePrev --> Compare: 下一帧
    note right of Compare: 仅Controller自身消费 边沿检测器

使用范围:只服务”车机主动发起配对”路径(BluetoothUnbondedDevicesPrefController)。对端发起配对(MiBluetoothPairingRequest)不走此链路。

5. 冲替语义总结

“冲替”=选已连设备A,断A,让位给新设备。新设备”连什么”由调用方决定:

  • 对端配对(场景1):A 断后不连新,dismiss 时取 mPendingPairRequestIntent 启动配对弹窗完成 PIN/passkey 确认。
  • 主动连接(3a/3b/3c):A 断后延时500ms performConnect 新设备(已 BONDED 不需再配对)。

三类共享同一 Picker UI 与”选A→断A”骨架,差异只在 autoConnectDEVICE_PAIR_INTENT 两参数。

mPendingPairRequestIntent 接力链buildDevicePickerIntent:96 putExtra(DEVICE_PAIR_INTENT, pairingIntent) → 目标 onCreate(Activity:118/Fragment:99) 从 extra 取出赋字段 → dismiss:169 取出调 getPairingDialogIntent 拉起配对弹窗。MiBluetoothPairingRequest 路径填入,其余4处传 null。

取消保护cancelPickDevice:141-150 检测 mPairingRequestDevice.bondState != BOND_BONDEDperformUnpair()——对端配对路径用户选”取消”时主动撤回配对请求,避免半配对态遗留。

6. 对抗自查

  1. 行号区分 if-check vs call:5 处”调用点”务必指 displayBluetoothDevicePicker(...) 实际行(:90/:526/:142/:295/:111),非 if 判定行。
  2. 五处三类:02 文档”三处”漏 BluetoothBondedDevicesPrefController:295(主页已配对点连接入口)。
  3. DevicePairStageEvent 非 stage 枚举:是三元组瞬时值 + 边沿检测器;事件源是 mPairingCachedDevice 状态变更(非”生产者=消费者=自身”)。
  4. 对讲机门槛:第2处 isTwoWayRadio(MAC CC:72:86),普通手机该路径不触发 Picker。
  5. 断旧同步、连新延时500msdisConnectChooseDevice:225 同步断,doConnectpostDelayed(500) 连新。