64 · 连接与断开 — 从用户点击到 framework 全链路走读
源码基线 f3dif。交叉引用:21-settingslib架构与事件流 §3 connect 链三栏对照、31-连接与自动连接策略、68 §2 3187 案例 本篇讲清一件事:用户在车机上点一下蓝牙设备,到耳机里出声,中间每一步代码。
0. 全链路鸟瞰(先背地图再看细节)
flowchart TD U[用户点击设备条目] --> UI1["BluetoothBondedDevicesPrefController<br/>handlePreferClick :176-198<br/>isBusy 守卫 :179"] UI1 -->|未连接| UI2["connectMis :203-217<br/>MIS 复合连接确认(存在则弹框)"] UI2 --> UI3["connectDevice :264-320<br/>双蓝牙上限判定"] UI3 -->|≥2 已连 HFP/A2DP| PICK["MiBluetoothPickerDialogActivity<br/>冲替弹窗 :313-318"] UI3 -->|未达上限| EXT["MiBluetoothExt.performConnect :16-24<br/>① sendConnectNewDeviceBroadcast :17<br/>② cachedDevice.connect() :19"] EXT --> C1["CachedBluetoothDevice.connect() :503"] C1 --> C2["ensurePaired() :634<br/>BOND_NONE → startPairing() :643<br/>(取消扫描 + createBond)并 return"] C2 -->|已配对| C3["connectDevice() :596(mProfileLock 内)"] C3 -->|mProfiles 空| GIVE_UP["log 'Maybe we will connect later'<br/>直接 return :599-609(等 ACTION_UUID)"] C3 -->|mProfiles 非空| FW["mDevice.connect() :611<br/>framework 整机连接:按各 profile 的<br/>connectionPolicy 一次连全部 allowed profile"] C3 -.CSIP 组.-> REC["member.connect() 递归 :612-617"] FW --> EVT["profile 服务逐个建立连接<br/>每条 profile 发 ACTION_CONNECTION_STATE_CHANGED"] EVT --> RB["状态回灌(§4)→ UI 刷新"]
1. UI 层入口:三道门卫
用户点设备条目(createDevicePreference :141-201 挂的监听),进 handlePreferClick(:176-198)前后的守卫:
| 门卫 | 位置 | 拦什么 |
|---|---|---|
| isBusy 守卫 | :179 | isBusy()(连接中/断开中/配对中)时点击无效——防止重复发起(3205 修复后还有 300ms setEnabledWithDebounce 防抖,见 34 篇) |
| MIS 复合连接确认 | connectMis :203-217 | 该设备同时是 MIS 互联复合连接目标时,先弹 ConnectionUtils.confirmConnectWhenMisExist 确认框(业务层,settingslib 无感知) |
| 双蓝牙上限 | connectDevice :264-320 | 已连接 HFP/A2DP 设备数 < DOUBLE_BLUETOOTH_DEVICE_LIMIT(=2) 才直连;达上限走 Picker 冲替(:313-318);非手机设备(无 HFP/A2DP)放行(:276-283) |
通过后 MiBluetoothExt.performConnect(MiBluetoothExt.kt:16-24)做两件事:① 发 sendConnectNewDeviceBroadcast(自有广播,通知互联模块);② 调 cachedDevice.connect()——从这里开始进入本系列主角。
详情页的”连接”按钮走
BluetoothOperationsController.connectDevice(:122-148),逻辑同上(上限判定 :132,picker :142-147)。
2. settingslib 层:connect() 三段式
2.1 ensurePaired()(:634-641)— 先确保证件
private boolean ensurePaired() {
if (getBondState() == BluetoothDevice.BOND_NONE) {
startPairing(); // :643-650
return false; // 本轮到此为止,等配对成功的广播再自动连
}
return true;
}startPairing()(:643-654):先取消扫描再 createBond()——扫描和配对抢无线资源,并发会互相干扰。配对成功后由 onBondingStateChanged BONDED 分支的 isBondingInitiatedLocally() → connect() 自动续上(:1192-1194,详见 66 章)——用户点一次,配对+连接全自动接力。
2.2 connectDevice()(:596-619)— 核心三行
// mProfileLock 内
if (mProfiles.isEmpty()) { // :599
log("Maybe we will connect later"); return; // 放弃,等 ACTION_UUID
}
mDevice.connect(); // :611 ← 就这一行,真正的连接
if (groupId 有效) member.connect(); // :612-617 CSIP 组员递归为什么 mProfiles 空就直接放弃? 注释(:600-606)写明:配对刚完成时 SDP 还没跑完,UUID 还没到,此时硬连会失败;等 ACTION_UUID 广播到达 onUuidChanged(),那里有自动补连机制(66 §3)。这不是偷懒,是与 carkit 配对期 UUID 竞态的正确规避。
mDevice.connect() 是什么? framework BluetoothDevice.connect()(@SystemApi,Android 13/API 33 前后引入的整机连接 API):一次性按每条 profile 的 connectionPolicy 连接所有 allowed 的 profile。也就是说 settingslib 不逐条 profile 发指令——“连哪些”的决策数据(policy)早就通过 setEnabled 写进了 framework 数据库,connect() 只是”执行”。
API 演进化石:老版本叫
connectAllEnabledProfiles()(dcddif:387-402 还在,经mActiveAdapter间接层);f3dif 里这个名字和connectProfile(profile)都已删除,只剩一个无人调用的connectInt(:621-632,profile.setEnabled(mDevice, true))作化石。读网上老资料看到这些方法名,别在 f3dif 里找,找不到。
2.3 失败处理在哪?
connect() 链本身没有重试——connectInt 里 setEnabled 失败仅 Log.i("Failed to connect ...")(:631)。真正的”失败呈现”是被动等出来的:每条 profile CONNECTING 时 60s 看门狗上弦(:298-301),60 秒内没到 CONNECTED 就置 mIs*ProfileConnectedFail(62 §8),UI 显示连接失败。同步调用的失败 + 异步等待的超时,两套机制各管一段。
3. 断开:disconnect() 与 PBAP 的教训
public void disconnect() { // :456-478(mProfileLock 内)
if (groupId 有效) member.disconnect(); // :458-463 CSIP 组员递归断开
if (LeAudioSharing flag) removeBroadcastSource(自己); // :465-467 清广播源
mDevice.disconnect(); // :468 framework 整机断开
// (锁外)PBAP server 兜底:
mPbapProfile?.setEnabled(mDevice, false); // :470-477
}:470-472 的注释是教材级知识点:
“some CK/Hs do not disconnect PBAP connection when HF connection is brought down”——有些车载套件/耳机断开 HFP 时不会顺手断 PBAP(协议栈实现不规范),所以 settingslib 要替它们兜底,手动把 PBAP server 关掉。跨厂商蓝牙生态的防御性编程范例。
- 单条 profile 断开:
disconnect(profile)(:480-486)=profile.setEnabled(mDevice, false)。 - UI 侧
performDisConnect(MiBluetoothExt.kt:32-48)在disconnect()(:37)之外再补发com.android.micar.settings.DEVICE_DISCONNECT自有广播(:39-42)通知互联模块——settingslib 不知道的事,App 层用自有广播补。 - UI 层断开前还有二次确认(
showDisConnectConfirmDialog:239-262 / 详情页 MIS 确认)。
4. 状态回灌:连接结果怎么回到 UI
连接指令发出去只是故事的一半,另一半是事件回流(详见 65 章,此处给 profile 状态这一条):
framework profile 服务建立/断开一条连接
→ 发 ACTION_CONNECTION_STATE_CHANGED 广播(带 EXTRA_DEVICE/STATE/PREVIOUS_STATE)
→ mProfileBroadcastReceiver 收到(BEM:74,注册于 registerProfileIntentReceiver :171-173)
→ 查 mHandlerMap 命中该 profile 的 StateChangedHandler(LBPM `addProfile` :302-306 统一注册,如 HfpClient 块 :168-173)
→ onReceiveInternal(LBPM:344-453):
findDevice 为 null 则 addDevice "found new device"(:337-340)
→ cachedDevice.onProfileStateChanged(mProfile, newState)(:434)
├─ CONNECTED → 加入 mProfiles(:344-352)+ 撤看门狗(:295-297)
├─ CONNECTING → 看门狗上弦 60s(:298-301)
├─ DISCONNECTED→ 移出 mProfiles(:363)+ 真失败判定(:307-331)
└─ TURNING_OFF → 直接 return 吞掉(:282-288,"Turninig" 拼错的那个分支)
→ cachedDevice.refresh()(:449)
→ dispatchProfileConnectionStateChanged(:450-451 → BEM:232-254,遍历 BluetoothCallback)
→ UI 三层回调(Controller/条目)刷新条目
注意 StateChangedHandler.onReceiveInternal 里 DISCONNECTED←CONNECTING 的日志(LBPM:347-350 “Failed to connect”)——排”点了没反应”问题时这是第一锚点。
5. 逐 profile 的开关:connectionPolicy 语义
setEnabled 不是”立即连接/断开”,而是”改策略”。以 HfpClientProfile.setEnabled(:152-166)为例:
- 开 = 仅当现值 < ALLOWED 时写
CONNECTION_POLICY_ALLOWED(幂等写保护:已是 ALLOWED 就不再写); - 关 = 写
CONNECTION_POLICY_FORBIDDEN,并断开。
逐一核对全部 7 个 profile 的 setEnabled(A2dp/A2dpSink/Headset/HfpClient/PbapClient/HearingAid/LeAudio):全部只调 setConnectionPolicy,settingslib 侧不触发任何连接动作(“开=顺手触发连接”是老 AOSP setPreferred 时代的残留语义,f3dif 已无此行为)——真正的连接由 connect() 链(§2)或 framework 自动回连(66 章)发起。UI 详情页类注释里的 “attempt a connection”(:35-38)描述的是业务效果,不是 setEnabled 的直接行为。
策略持久化完全在 framework 蓝牙 APK 侧(settingslib 内无任何本地持久化代码):mService.setConnectionPolicy(device, ...)(:159)binder 进 com.android.bluetooth。isEnabled(device)(:44 接口)= getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN——UI 勾选态的真源。
接口名变迁:手机老 AOSP 的
setPreferred/getPreferred在本库已全量改名setEnabled/getConnectionPolicy;connectionState现名getConnectionStatus;接口没有isConnectable/connect/disconnect方法(LocalBluetoothProfile.java:26-82接口面)。金标准已核实:isAutoConnectable不在 connect 链上——f3dif 21 处命中(三 flavor 全仓 60 处)全是定义与覆写,零调用点(21 篇 §8 同结论)。
车机 UI 上唯一暴露给用户的逐 profile 开关是”通讯录同步”(PBAP Client):详情页 BluetoothDeviceProfilesPreferenceController.handlePreferenceChanged(:90-102)直接调 mPbapClientProfile.setEnabled(device, newValue)(:94-97,UI 层唯此一处 profile.setEnabled 调用)——绕过 CBD 的包装直接操作 settingslib profile 对象;仅手机类设备且非对讲机可见(isPhoneDevice && !isTwoWayRadio,:68-69,正是 68 §8 MANNPROB-2846 对讲机旁路的落点)。其余 profile 无独立开关,由 CBD.connect() 一次全连。
6. 时序总账(一张图记住全流程)
sequenceDiagram participant U as 用户 participant Ctrl as Controller(UI) participant Ext as MiBluetoothExt participant CBD as CachedBluetoothDevice participant FW as framework/蓝牙APK U->>Ctrl: 点击设备 Ctrl->>Ctrl: isBusy 守卫 + MIS 确认 + 双蓝牙上限 Ctrl->>Ext: performConnect(cachedDevice) Ext-->>FW: sendConnectNewDeviceBroadcast(通知互联) Ext->>CBD: connect() CBD->>CBD: ensurePaired()(未配对→createBond 后止) CBD->>CBD: connectDevice()(mProfiles 空→放弃等UUID) CBD->>FW: mDevice.connect()(整机连接) loop 每条 profile FW-->>CBD: ACTION_CONNECTION_STATE_CHANGED 广播 CBD->>CBD: onProfileStateChanged(看门狗上弦/撤弦) CBD->>Ctrl: refresh → dispatch → UI 刷新 end
7. 自测
- 用户点击后、真正连接前,UI 层有哪三道门卫?(isBusy / MIS 确认 / 双蓝牙上限)
-
mProfiles为空时connectDevice()做什么?为什么?(直接 return 等 UUID——SDP 未完成的竞态规避) - f3dif 的整机连接靠哪个 API?(
BluetoothDevice.connect(),不是旧名 connectAllEnabledProfiles) - 连接失败怎么被发现的?(同步调用只打日志;失败呈现靠 60s 看门狗异步置位)
- 断开时为什么手动关 PBAP server?(部分 CK/Hs 断 HFP 不带断 PBAP——:470-472 注释)
-
setEnabled的语义是”立即连断”还是”改策略”?(改 connectionPolicy,持久化在 framework 侧)
下一篇 65-属性刷新与回调分发:§4 那条”回到 UI”的路,展开讲透。