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 守卫:179isBusy()(连接中/断开中/配对中)时点击无效——防止重复发起(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() 链本身没有重试——connectIntsetEnabled 失败仅 Log.i("Failed to connect ...")(:631)。真正的”失败呈现”是被动等出来的:每条 profile CONNECTING 时 60s 看门狗上弦(:298-301),60 秒内没到 CONNECTED 就置 mIs*ProfileConnectedFail62 §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.bluetoothisEnabled(device)(:44 接口)= getConnectionPolicy(device) > CONNECTION_POLICY_FORBIDDEN——UI 勾选态的真源。

接口名变迁:手机老 AOSP 的 setPreferred/getPreferred 在本库已全量改名 setEnabled/getConnectionPolicyconnectionState 现名 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”的路,展开讲透。