70 · 总览与闭环地图 — active 设备的四象限

源码基线:dcddif(dcd flavor,骁龙)为主,xcddif(xcd/global 共用)差异显式标注;dev 分支,2026-08-23 核对。 ⚠️ 本仓 dev 分支 settingsLibAndroid/src/只有 dcddif / xcddif 两份副本,f3dif 不存在(全仓 find + build.gradle:46-67 核实)。60-系列的 f3dif 锚点来自 A17 分支(dev_a17_0610)历史基线,其行号在本仓不可直接复用。 交叉引用:67-active设备与多设备(f3dif 基线概念篇)、21-settingslib架构与事件流31-连接与自动连接策略

0. 这个系列是什么

围绕一个问题的完整解剖:“当前音频从哪台蓝牙设备出”(active device)在 MiCarSettings 里是怎么查询、怎么监听、怎么设置、怎么显示的。

四个象限 = 四章,每章一个方向:

象限一句话
查询(pull)71fetchActiveDevices() 主动向各 profile 对账,写 mIsActiveDevice* 布尔位
监听(push)723~4 条 ACTION_ACTIVE_DEVICE_CHANGED 广播 → onActiveDeviceChanged 值变才分发
设置(set)73setActive() 逐行精讲 + 车机真实音源切换链(不是 setActive)
消费(consume)74active 位 → AOSP 文案管道(车机死链)与 MiCar 自建三态摘要

67 章的分工:67 是 f3dif 基线上的概念总览(四维 active、CSIP 组、换壳、电量);本系列是 dcddif/xcddif 双基线的逐代码深讲 + 车机实际用法考古(哪些链路活着、哪些是死链)。

1. 三十分钟概念地基(细节见各章)

  • 连接 ≠ 活跃:多台设备可同时连接,但每类音频同一时刻只有一个(助听器/LeAudio 一对)输出目标。连接=入会,活跃=拿着话筒。
  • 单/双 active 并存:A2DP/HFP 用 equals(getActiveDevice())(独木桥,一台);HearingAid/LeAudio 用 getActiveDevices().contains(mDevice)(双人桥,左右耳一对)。
  • pull 与 push 互补fetchActiveDevices 解决”出生/重算时错过广播”;onActiveDeviceChanged 解决”运行中实时性”。两者写同一组 mIsActiveDeviceA2dp/Headset/HearingAid(/LeAudio) 布尔位(dcddif CachedBluetoothDevice.java:147-149 / xcddif :126-129)。
  • push 链不经 LocalBluetoothProfileManager:EventManager 直达 CBD;ProfileManager 只出现在 pull 侧(取 profile 对象)。

2. 闭环地图

flowchart TB
    SYS["系统蓝牙栈 (com.android.bluetooth)"] -->|"ACTION_ACTIVE_DEVICE_CHANGED<br/>dcddif 3 条 / xcddif 4 条(多 LE_AUDIO)"| BEM
    subgraph settingslib["settingslib (dcddif/xcddif)"]
        BEM["BluetoothEventManager<br/>ActiveDeviceChangedHandler"] -->|"findDevice 按 MAC 线性查表<br/>isActive = Objects.equals 推导<br/>旧设备 false 是遍历全表捎带"| CBD["CachedBluetoothDevice<br/>mIsActiveDevice* 四维布尔位<br/>(值变才 dispatchAttributesChanged)"]
        PULL["fetchActiveDevices() 拉取对账<br/>fillData / onProfileStateChanged<br/>/ switchSubDeviceContent 触发"] -->|写同一组布尔位| CBD
        SET["CBD.setActive()<br/>⚠️ 全仓零调用(死链)"] -->|"profile.setActiveDevice →<br/>Adapter.setActiveDevice(type) 经 AdapterUtil 路由"| SYS
    end
    CBD -->|"Callback.onDeviceAttributesChanged<br/>(主线程分发)"| UI
    subgraph 车机UI层["micarConnectionSettings"]
        UI["列表条目 refreshDeviceUi<br/>active 相关为空实现/不显示角标"]
    end

车机真实的音源切换路径(替代 setActive 的实际交互,详见 73):

用户点击新设备条目
→ BluetoothOperationsController.handlePreferenceChanged:144
→ connectDevice:115
→ MiBluetoothExt.kt:16 performConnect
→ CachedBluetoothDevice.connect():359
→ connectAllEnabledProfiles (dcddif CBD:402)
→ 系统栈建连并裁决 active(达到 DOUBLE_BLUETOOTH_DEVICE_LIMIT=2 时先弹 Picker 选冲替)
→ 广播回读 → 布尔位更新

3. 本系列三个最重要的发现(车机视角)

  1. setActive() 全仓零调用(dcddif :544-568 / xcddif :730-761 定义即全部,java/kt 多模式 grep 核实)。AOSP 手机”点击设备设为活跃输出”的链路在车机 UI 被裁掉;音源切换的等价交互是”连接新设备”,active 裁决权在系统蓝牙栈。profile 层 setActiveDevice 也不走 AOSP 的 mService 直调,而是 BluetoothAdapter.setActiveDevice(type)BluetoothAdapterUtil.getAdapterByProfile 双蓝牙路由(A2DP=AUDIO、HFP=PHONE_CALL、HA 按通话态动态选、LeAudio=ALL;LeAudio 构造用 getDefaultAdapter 是路由例外)。
  2. AOSP 的 active→文案管道整体死链getCarConnectionSummary() 全仓无调用者;getConnectionSummary() 唯一调用点是 getSubDeviceSummary()(dcddif CachedBluetoothDeviceManager.java:124-129),后者同样零调用。MiCar 列表副标题是自建三态”已连接/正在连接/已保存”(MiCarBluetoothBondedDevicePreference.kt:92-108),无 active 角标;UI 层 BluetoothPreferenceController.onActiveDeviceChanged/onAudioModeChanged 是空方法体(:162-168)。
  3. 闭环活着的一段是”监听+拉取”:广播链与对账链持续维护四维布尔位(它们是 getConnectionSummary 的输入,也是未来任何消费方的地基),只是当前 UI 面上没有直接消费者。排查”音源不对”问题时,布尔位仍是有用的中间观测点(配合 logcat CachedBluetoothDevice tag)。

4. 关键锚点速查(红队核对版)

事实dcddifxcddif
active 广播注册BluetoothEventManager.java:114-117(3 条):136-141(4 条,含 LE_AUDIO)
isActive 推导(Objects.equals)BluetoothEventManager.java:228:316-329 另有 CSIP 组员→主设备回退
onActiveDeviceChangedCachedBluetoothDevice.java:640-663(三分支):835-868(四分支,LE_AUDIO :856-858)
四维布尔位:147-149(三维):126-129(四维,多 LeAudio)
fetchActiveDevices:767-783:976-993(LeAudio :989-992)
isActiveDevice:678-691(default 日志文案是 “getActiveDevice:“,与方法名不符的化石):883-898
profile 层读 activemService 代理:A2dpProfile.java:175-178 / HeadsetProfile.java:137-142(返 null)、HearingAidProfile.java:178-181(返空 List)全改 BluetoothAdapter.getActiveDevices(profile)
setActive:544-568(三段,零调用):730-761(四段,LeAudio :753-759)
“使用中”文案三分支getConnectionSummary :1123-1127:1431-1434(LeAudio 不受 isOnCall 约束)
四下标数组实值"" / ",使用中" / ",使用中(媒体)" / ",使用中(手机)" res/values-zh-rCN/arrays.xml:152(两 flavor 一致)同左
isOnCallUtils.java:418-424(MODE_RINGTONE/IN_CALL/IN_COMMUNICATION)

5. 学习路径

  • 按链路读:71 查询 → 72 监听 → 73 设置 → 74 消费(自然顺序,每章自带交叉引用)。
  • 排障救急:先读 74 §排障(界面与实际输出不一致的断点清单)+ 40-日志取证速查
  • 只想知道车机怎么切音源:直奔 73 §UI 触发链

6. 事实基线声明与维护约定

  • 本系列全部锚点在 2026-08-23 于 dev 分支对源码逐个核对;研究过程由 4 个并行 agent 完成,关键论断经主线独立 grep 复核(setActive 零调用、getConnectionSummary 消费链),并经红队对抗审查(记录见 _对抗核验记录-70篇.md)。
  • 跨 flavor 论断以”dcddif/xcddif”显式标注;f3dif 相关内容一律引用 60-系列的 A17 分支基线,不在本仓复标行号。
  • 代码演进后(如 setActiveDevice API 或广播条目变化)请同步更新对应章节与本表。

7. 自测清单

  • 连接和活跃的区别?单 active 和双 active 分别对应哪些 profile、用什么判定?
  • push 链上 isActive 是从哪个 extra 读的吗?(不是——Objects.equals 推导)
  • 一次 active 切换广播,在什么限定下实际触发属性分发的只有新旧两台?副耳/陈旧位场景为什么不成立(72 §4.1 边界框)?
  • 车机上用户切音源的实际链路是什么?setActive() 为什么用不上?
  • mIsActiveDevice* 四维布尔位当前在 UI 上有直接消费者吗?维护它的两条链分别是什么?
  • 车机版摘要为什么可以不看 isOnCall 而手机版要看?

8. 交叉引用