32 · 双蓝牙上限与 Picker 冲替机制
核心文件:
ConnectionConstants.kt、MiBluetoothPickerDialogActivity.kt、MiBluetoothPickerDialogFragment.kt、DevicePairStageEvent.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 文档原遗漏此入口) |
| 3c | CarPlay 配对后选蓝牙 | MiCarplayConnectTypeChooseDialogFragment.kt | :106 | :111 | 同 3a |
对讲机识别 = MAC 前缀 CC:72:86(MiBluetoothUtils.kt:37 RADIO_DEVICE_ADDRESS_PREFIX,isTwoWayRadio :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 决定语义:
| 调用方 | autoConnect | DEVICE_PAIR_INTENT | 语义 |
|---|---|---|---|
| PairingRequest:90 | false | 非空 | 选A→断A→dismiss 接力启动配对弹窗(不连新设备,因未配对) |
| 其余4处(:526/:142/:295/:111) | true | null | 选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 注册方与消费方都在 BluetoothUnbondedDevicesPrefController(mPairingCallback :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”骨架,差异只在 autoConnect 与 DEVICE_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_BONDED 则 performUnpair()——对端配对路径用户选”取消”时主动撤回配对请求,避免半配对态遗留。
6. 对抗自查
- 行号区分 if-check vs call:5 处”调用点”务必指
displayBluetoothDevicePicker(...)实际行(:90/:526/:142/:295/:111),非 if 判定行。 - 五处三类:02 文档”三处”漏
BluetoothBondedDevicesPrefController:295(主页已配对点连接入口)。 - DevicePairStageEvent 非 stage 枚举:是三元组瞬时值 + 边沿检测器;事件源是
mPairingCachedDevice状态变更(非”生产者=消费者=自身”)。 - 对讲机门槛:第2处
isTwoWayRadio(MACCC:72:86),普通手机该路径不触发 Picker。 - 断旧同步、连新延时500ms:
disConnectChooseDevice:225同步断,doConnect内postDelayed(500)连新。