73 · 设置侧 — setActive 与车机 UI 触发链
本篇讲清什么
上一篇(67 章)讲了”什么是 active 设备、mIsActiveDeviceA2dp 这些布尔位怎么用”。本篇往下钻一层,回答三个问题:
- 设置侧怎么把一台设备设为 active?——逐行精讲
CachedBluetoothDevice.setActive()和它下面四个 profile 的setActiveDevice()。 - 车机 UI 上用户做什么操作会走到这条链?——这是本篇最有”车机特色”的部分,结论可能反直觉:本仓 UI 从不直接调用
setActive(),车机上”切音源”的交互等价物是”连接新设备(必要时先断旧设备)“。我们把完整 UI 链路还原出来。 - 失败了怎么办、去哪看日志?
阅读前置:需要知道 64 章的连接链路(本篇会反复对照),以及”profile”是指 A2DP/HFP 等蓝牙规格在设置侧的包装类。
一个先说清的事实:本仓 base/settingsLibAndroid/src 下只有 dcddif 和 xcddif 两个 flavor 源集,不存在 f3dif 目录(ls src/ 只有这两项;全仓 find -name CachedBluetoothDevice.java 也只命中这两份)。任务清单里提到的”f3dif 版”,在本仓对应的是 xcddif——它是带 LeAudio 第四段的版本。本文所有”f3dif”字样均按 xcddif 处理并如实标注。
一、setActive() 逐行精讲
1.1 dcddif 版(三段 profile)
dcddif CachedBluetoothDevice.java:544-568,原样贴出:
/**
* Set this device as active device
* @return true if at least one profile on this device is set to active, false otherwise
*/
public boolean setActive() {
boolean result = false;
A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
if (a2dpProfile != null && isConnectedProfile(a2dpProfile)) {
if (a2dpProfile.setActiveDevice(getDevice())) {
Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this);
result = true;
}
}
HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
if ((headsetProfile != null) && isConnectedProfile(headsetProfile)) {
if (headsetProfile.setActiveDevice(getDevice())) {
Log.i(TAG, "OnPreferenceClickListener: Headset active device=" + this);
result = true;
}
}
HearingAidProfile hearingAidProfile = mProfileManager.getHearingAidProfile();
if ((hearingAidProfile != null) && isConnectedProfile(hearingAidProfile)) {
if (hearingAidProfile.setActiveDevice(getDevice())) {
Log.i(TAG, "OnPreferenceClickListener: Hearing Aid active device=" + this);
result = true;
}
}
return result;
}逐段拆解:
第一道门卫:profile != null。 mProfileManager.getA2dpProfile()(dcddif LocalBluetoothProfileManager.java:412-414)是直接 return mA2dpProfile;,不检查 isProfileReady()(对比同文件里 getA2dpSinkProfile() 会判 ready 才返回)。所以这里的 null 检查防的是”这个 flavor 根本没创建该 profile 对象”(例如某些车型配置下 profile 列表不含 HearingAid),而不是”profile proxy 未就绪”——就绪与否在这一层不拦。
第二道门卫:isConnectedProfile(profile)。 dcddif CachedBluetoothDevice.java:718-721:
public boolean isConnectedProfile(LocalBluetoothProfile profile) {
int status = getProfileConnectionState(profile);
return status == BluetoothProfile.STATE_CONNECTED;
}只有 STATE_CONNECTED(已连上)才放行。注意 STATE_CONNECTING 不放行——正在连接中的设备不能发起 setActive,这是”必须先连上才谈得上激活”的语义。这也回答了问题 4 的一半:未连接的 profile 段直接跳过,不是报错,是静默不发起。
三段为什么是这个顺序、为什么每段独立判断? A2DP(媒体音频)→ Headset/HFP(电话音频)→ HearingAid(助听器,同时接管媒体+通话)。一台手机通常同时连 A2DP+HFP,所以前两段都会命中,各发一次 setActiveDevice,分别把”媒体通道的 active”和”通话通道的 active”都指到这台设备上。助听器是特殊设备才有的 profile,普通手机第三段门卫不过,跳过。
setActiveDevice(getDevice()) 返回值语义:命令已被接受,不是已生效。 返回 true 只代表 binder 调用成功、蓝牙栈受理了这条请求;真正生效的标志是稍后系统广播 ACTION_ACTIVE_DEVICE_CHANGED(见第五节回环)。⚠️ “受理 vs 生效”的区分是基于 AOSP 框架公开行为与广播回环设计的推断——栈内同步/异步生效细节不在本仓视野,若框架在某版本改为同步生效,此表述需修订。所以:
result 的 OR 语义: result 初始 false,任何一段成功就置 true,且不因某段失败而中断后续段——三段之间没有 else/return。注释也写明 @return true if at least one profile on this device is set to active。“至少一个 profile 受理”就算这次 setActive 成功。反过来说:三段全被门卫拦下或全被栈拒绝,才返回 false。
每步的 Log: 成功的段打 Log.i(TAG, "OnPreferenceClickListener: A2DP active device=" + this)(TAG = "CachedBluetoothDevice",dcddif CachedBluetoothDevice.java:104)。Headset 段是 "OnPreferenceClickListener: Headset active device=",HearingAid 段是 "OnPreferenceClickListener: Hearing Aid active device="。日志文案里的 “OnPreferenceClickListener” 是 AOSP 手机设置的历史残留——原版这段逻辑确实挂在列表项点击监听器里,移植时方法体搬进了 setActive() 但日志字符串没改。失败的段没有任何日志(门卫不过静默跳过,栈拒绝也只在系统侧有日志)。
1.2 xcddif 版(四段,多 LeAudio)
xcddif CachedBluetoothDevice.java:730-761。前三段与 dcddif 逐字相同(行号平移),差异只在末尾追加了第四段(xcddif CachedBluetoothDevice.java:753-759):
LeAudioProfile leAudioProfile = mProfileManager.getLeAudioProfile();
if ((leAudioProfile != null) && isConnectedProfile(leAudioProfile)) {
if (leAudioProfile.setActiveDevice(getDevice())) {
Log.i(TAG, "OnPreferenceClickListener: LeAudio active device=" + this);
result = true;
}
}
return result;LeAudio(LE Audio,蓝牙低功耗音频)是新世代音频架构,一个 profile 同时覆盖媒体与通话,所以它用 ACTIVE_DEVICE_ALL(见下节)。xcddif 的 TAG 同样是 "CachedBluetoothDevice"(xcddif CachedBluetoothDevice.java:71)。
配套地,xcddif 的 onActiveDeviceChanged() 也多了 case BluetoothProfile.LE_AUDIO 分支更新 mIsActiveDeviceLeAudio(xcddif CachedBluetoothDevice.java:856-858,字段声明在 :129);dcddif 版(dcddif CachedBluetoothDevice.java:640-663)只有 A2DP/HEADSET/HEARING_AID 三个 case,未知 profile 打 Log.w(TAG, "onActiveDeviceChanged: unknown profile ...")。
二、profile 层 setActiveDevice 逐个精讲
先看一个共性结构。四个 profile 的 setActiveDevice 长得几乎一样:
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(<PROFILE_TYPE>)
: mBluetoothAdapter.setActiveDevice(device, <PROFILE_TYPE>);
}三个值得停下来看的点:
- 判空的是
mBluetoothAdapter,不是mService。 这和 AOSP 原版(走mService.setActiveDevice(device)即 BluetoothA2dp proxy)不同,是 MiCar 的双蓝牙改造(// MICAR-PORTING-START标记)。构造时mBluetoothAdapter = BluetoothAdapterUtil.getAdapterByProfile(getProfileId())就已赋值,字段还是private final(dcddif A2dpProfile.java:50),所以这个判空实际是防御式写法,正常运行几乎不会命中。也正因为不走 mService,profile proxy 尚未 bind 完成时 setActiveDevice 依然能发出去——这是与getActiveDevice()(走 mService,null 时直接返回 null)的一个行为差异。 device == null的分支是”取消激活”:调removeActiveDevice(type)把当前 active 清掉(传 null 给本方法即”不再指定任何 active 设备”)。- 跨进程边界:
mBluetoothAdapter.setActiveDevice(device, type)是android.bluetooth.BluetoothAdapter(被 MiCar 框架扩展过的版本)上的调用,从这里 binder 进蓝牙服务进程。系统侧(adapter 服务如何路由到具体 profile 服务、如何广播)不在本仓视野,止步于此。
2.1 A2dpProfile(媒体音频)
dcddif A2dpProfile.java:166-173(xcddif 版在 xcddif A2dpProfile.java:171-178,逐字相同):
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_AUDIO)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_AUDIO);
}- profile type 常量:
ACTIVE_DEVICE_AUDIO(=1,媒体音频通道)。 - 构造路由:
dcddif A2dpProfile.java:105-107/xcddif A2dpProfile.java:110-112,都包在// MICAR-PORTING-START/END里:// MICAR-PORTING-START mBluetoothAdapter = BluetoothAdapterUtil.getAdapterByProfile(getProfileId()); // MICAR-PORTING-ENDBluetoothAdapterUtil是 framework 类(import android.bluetooth.BluetoothAdapterUtil;,dcddif A2dpProfile.java:25),本仓只有调用没有实现——“按 profile 找该用哪个蓝牙适配器”的路由逻辑在系统侧。
2.2 HeadsetProfile(HFP,电话音频)
dcddif HeadsetProfile.java:127-135(xcddif 同行号 127-135,逐字相同):
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_PHONE_CALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_PHONE_CALL);
}- profile type:
ACTIVE_DEVICE_PHONE_CALL(=2,通话通道)。与 A2DP 的 type 不同,正是这个差异让”媒体”和”通话”可以各自有独立的 active 设备。 - 构造路由同样带 MICAR-PORTING 标记(
dcddif HeadsetProfile.java:105-107/xcddif HeadsetProfile.java:105-107)。 - 一个 flavor 细节:
getActiveDevice()两版判空对象不同——dcddif 判mService(dcddif HeadsetProfile.java:137-141),xcddif 判mBluetoothAdapter。setActiveDevice 本身无此差异。
2.3 HearingAidProfile(助听器,带通话态动态路由)
dcddif HearingAidProfile.java:166-176(xcddif 在 xcddif HearingAidProfile.java:200-210,逐字相同):
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
int profiles = Utils.isAudioModeOngoingCall(mContext)
? ACTIVE_DEVICE_PHONE_CALL
: ACTIVE_DEVICE_AUDIO;
return device == null
? mBluetoothAdapter.removeActiveDevice(profiles)
: mBluetoothAdapter.setActiveDevice(device, profiles);
}与前两个 profile 的差别在第三行:助听器不写死 profile type,而是先问 Utils.isAudioModeOngoingCall(mContext) 当前是否通话中——通话中用 ACTIVE_DEVICE_PHONE_CALL,空闲用 ACTIVE_DEVICE_AUDIO。因为助听器场景下媒体和通话可能共用一条音频链,激活通道要跟着音频模式动态选。构造路由 dcddif HearingAidProfile.java:106 / xcddif HearingAidProfile.java:140,同样经 BluetoothAdapterUtil.getAdapterByProfile。
2.4 LeAudioProfile(仅 xcddif)
xcddif LeAudioProfile.java:165-172:
public boolean setActiveDevice(BluetoothDevice device) {
if (mBluetoothAdapter == null) {
return false;
}
return device == null
? mBluetoothAdapter.removeActiveDevice(ACTIVE_DEVICE_ALL)
: mBluetoothAdapter.setActiveDevice(device, ACTIVE_DEVICE_ALL);
}- profile type:
ACTIVE_DEVICE_ALL(=3,媒体+通话一把抓)。 - 构造与其他三个 profile 不同:
xcddif LeAudioProfile.java:108-117用的是BluetoothAdapter.getDefaultAdapter(),不是BluetoothAdapterUtil.getAdapterByProfile,也没有 MICAR-PORTING 标记(全文件 0 处)——LeAudio 目前不走双蓝牙按 profile 路由,固定用默认适配器。字段声明xcddif LeAudioProfile.java:54。 - 这个类的
getActiveDevices()也走mBluetoothAdapter.getActiveDevices(BluetoothProfile.LE_AUDIO)(xcddif LeAudioProfile.java:174-179),与其他 profile 走 mService 的做法不同。
2.5 四个 profile 一张表
| profile | flavor | 位置 | profile type | 构造路由 | MICAR-PORTING 标记 |
|---|---|---|---|---|---|
| A2dpProfile | dcddif | A2dpProfile.java:166-173 | ACTIVE_DEVICE_AUDIO | getAdapterByProfile(:105-107) | 有(2 处) |
| A2dpProfile | xcddif | A2dpProfile.java:171-178 | ACTIVE_DEVICE_AUDIO | getAdapterByProfile(:110-112) | 有(4 处) |
| HeadsetProfile | dcddif | HeadsetProfile.java:127-135 | ACTIVE_DEVICE_PHONE_CALL | getAdapterByProfile(:105-107) | 有(2 处) |
| HeadsetProfile | xcddif | HeadsetProfile.java:127-135 | ACTIVE_DEVICE_PHONE_CALL | getAdapterByProfile(:105-107) | 有(4 处) |
| HearingAidProfile | dcddif | HearingAidProfile.java:166-176 | 通话中 PHONE_CALL,否则 AUDIO | getAdapterByProfile(:106) | 有(4 处) |
| HearingAidProfile | xcddif | HearingAidProfile.java:200-210 | 通话中 PHONE_CALL,否则 AUDIO | getAdapterByProfile(:140) | 有(4 处) |
| LeAudioProfile | 仅 xcddif | LeAudioProfile.java:165-172 | ACTIVE_DEVICE_ALL | getDefaultAdapter(:114) | 无(0 处) |
三、UI 触发链(本篇重点)
3.1 先说结论:setActive() 在本仓是”无调用方”的方法
对整个仓库(含 settingslib、全部 settingsPage、settingsCommon)grep setActive(),只有两处定义,零处调用:
base/settingsLibAndroid/src/xcddif/.../CachedBluetoothDevice.java:730: public boolean setActive() {
base/settingsLibAndroid/src/dcddif/.../CachedBluetoothDevice.java:544: public boolean setActive() {
micarConnectionSettings 模块里连 “setActive” 字符串都不出现(大小写不敏感 grep “active” 命中的全是 isVoiceAssistActive(语音助手可见即可说)、mIsControllerActive(controller 生命周期)、getActiveSubscriptionInfoList(蜂窝卡)等无关概念)。
也就是说:AOSP 手机设置里”点击已连接设备→设为活跃”的那条 OnPreferenceClickListener → setActive() 链路,在车机设置里被裁掉了。setActive() 是从 AOSP 带过来的保留方法,随时可以被未来代码启用,但当前 dev 分支上没有任何 UI 路径走到它。
3.2 那车机上用户”切蓝牙音源”实际发生什么?
车机的交互模型是:通过”连接”来表达”切换”。入口不止一个,殊途同归到 performConnect → CachedBluetoothDevice.connect()。以设备详情页的”连接”按钮为例:
第一环:用户在设备详情页点”连接”。 BluetoothOperationsController.handlePreferenceChanged()(micarConnectionSettings .../bluetooth/controller/BluetoothOperationsController.java:144)收到 PreferenceAction.Connect:
if (newValue == PreferenceAction.Connect) { // 连接设备
ConnectInfo connInfo = MisComplexSdk.getInstance().getConnectInfo();
if (ConnectionUtils.confirmConnectWhenMisExist(getContext(),
connInfo, getCachedDevice().getAddress(), false,
(dialog, which) -> {
if (which == DialogInterface.BUTTON_POSITIVE) {
connectDevice(getCachedDevice());
}
})) {
return false;
}
connectDevice(getCachedDevice());(若米家/妙想复合连接存在,先弹确认框,确认后才继续。)
第二环:connectDevice 判断要不要弹 Picker。 BluetoothOperationsController.java:115-140:
private void connectDevice(CachedBluetoothDevice cachedDevice) {
if (!MiCarSettingsExtKt.hasHfpOrA2dp(cachedDevice)) {
MiBluetoothExtKt.performConnect(cachedDevice,
() -> ConnectionUtils.updateLastConnectedDeviceWithNew(cachedDevice));
return;
}
// 是否需要展示picker供用户选择要切换的蓝牙设备
List<CachedBluetoothDevice> deviceList = ConnectionUtils.getConnectedHfpA2dpDevices(
getContext());
if (deviceList.size() < DOUBLE_BLUETOOTH_DEVICE_LIMIT) {
MiBluetoothExtKt.performConnect(cachedDevice,
() -> ConnectionUtils.updateLastConnectedDeviceWithNew(cachedDevice));
return;
}
...
MiBluetoothPickerDialogActivity.displayBluetoothDevicePicker(
true, true, cachedDevice, getFragmentController().getActivity());
}三条岔路:
- 设备连 HFP/A2DP 都没有(纯 IoT 类设备):直接连。
- 当前已连接的 HFP/A2DP 设备数 <
DOUBLE_BLUETOOTH_DEVICE_LIMIT(= 2,micarConnectionSettings .../connection/ConnectionConstants.kt:19,即车机最多同时连两台 HFP/A2DP 设备):还有空位,直接连。 - 已满:弹
MiBluetoothPickerDialogActivity设备选择器,让用户选”替换哪台”——这就是车机版”切换设备”的交互等价物。
第三环(直连岔路):performConnect → connect()。 micarConnectionSettings .../bluetooth/MiBluetoothExt.kt:16-24:
fun CachedBluetoothDevice.performConnect(appendAction: Runnable? = null) {
sendConnectNewDeviceBroadcast(device)
MLog.d("connect device: ${this.name}")
this.connect()
appendAction?.let {
MLog.d("execute append action when connect device")
ThreadPoolUtils.ASYNC.execute(it)
}
}connect() 在 dcddif CachedBluetoothDevice.java:359-367(签名 :359),内部 connectAllEnabledProfiles() → mActiveAdapter.connectAllEnabledProfiles(mDevice)(dcddif CachedBluetoothDevice.java:402)——注意这里走的是 mActiveAdapter(双蓝牙选中的适配器),把该设备所有已启用 profile 一并连上。连接成功后,由系统侧蓝牙栈决定把 active 指向谁(通常是新连上的这台),设置侧随后靠广播感知(第五节)。这就是”连接即切换”的闭环:设置侧从不点名 active,只操纵连接;active 的裁决权在系统。
第三环(Picker 岔路):用户选一台旧设备让位。 MiBluetoothPickerDialogFragment 把已连接设备列成 device_a/device_b 两个选项(MiBluetoothPickerDialogFragment.kt:152-171),点选后 onClick → doConnect(...)(:238)/确认框 → 断开被选中的旧设备 + 连接新设备(复合连接场景见 :280-297 的确认逻辑),最终同样落到 ConnectRunnable(:299-305):
class ConnectRunnable(private val mDevice: CachedBluetoothDevice) : Runnable {
override fun run() {
mDevice.performConnect {
ConnectionUtils.updateLastConnectedDeviceWithNew(mDevice)
}
}
}另一个等价入口:Carplay/蓝牙二选一弹窗。 从配对流程进入时,MiCarplayConnectTypeChooseDialogFragment.onClick()(MiCarplayConnectTypeChooseDialogFragment.kt:89-128)里 R.id.connection_bt 分支的逻辑与 connectDevice 完全同构(无 HFP/A2DP 直连、未满直连、满了弹 Picker),最终也是 mCacheDevice!!.performConnect { ... }(:100、:108)。
3.3 全链路图
flowchart TD A["用户点'连接'按钮<br/>设备详情页"] --> B["BluetoothOperationsController<br/>handlePreferenceChanged:144<br/>PreferenceAction.Connect"] B --> C{"confirmConnectWhenMisExist<br/>妙想复合连接确认"} C -- 确认/无 --> D["connectDevice:115"] C -- 取消 --> Z1["return false, 什么都不发生"] D --> E{"已连接 HFP/A2DP 设备数<br/>>= DOUBLE_BLUETOOTH_DEVICE_LIMIT?"} E -- "否(有空位)" --> F["MiBluetoothExt.performConnect<br/>MiBluetoothExt.kt:16"] E -- "是(已满)" --> G["MiBluetoothPickerDialogActivity<br/>设备选择器"] G --> H["用户选择被替换的旧设备"] H --> I["断开旧设备 + performConnect 新设备<br/>ConnectRunnable:299"] I --> F F --> J["CachedBluetoothDevice.connect():359"] J --> K["mActiveAdapter<br/>.connectAllEnabledProfiles:402"] K --> L["binder → 系统侧蓝牙栈<br/>(本仓视野结束)"] L --> M["系统决定 active 设备<br/>发 ACTION_ACTIVE_DEVICE_CHANGED"] M --> N["第 72/74 章的回环:<br/>onActiveDeviceChanged 更新布尔位 → UI 刷新"] O["另一入口: Carplay 选择弹窗<br/>MiCarplayConnectTypeChooseDialogFragment<br/>onClick:89, connection_bt 分支"] --> E P["CachedBluetoothDevice.setActive():544/730<br/>(dcddif/xcddif)"] -. "本仓零调用, 保留方法" .-> Q["A2dp/Headset/HearingAid/(LeAudio)<br/>.setActiveDevice → mBluetoothAdapter"]
3.4 UI 怎么显示 active?——不显示
已配对列表项的状态文案只有三态(MiCarBluetoothBondedDevicePreference.kt:93-108):
override fun getDeviceSummary(): String {
val isConnected = cachedDevice.isConnected
val isBusy = cachedDevice.isBusy
...
return if (isConnected) {
context.getString(R.string.bluetooth_bonded_device_connected)
} else if (isBusy) {
context.getString(R.string.bluetooth_bonded_device_connecting)
} else {
context.getString(R.string.bluetooth_bonded_device_saved)
}
}已连接 / 连接中 / 已保存,没有”活跃设备”的区分——车机 UI 的心智模型里只有”连着几台”,没有”哪台是 active”。(isActiveDevice(...) 的查询能力在 settingslib 里完备,dcddif CachedBluetoothDevice.java:678-691,只是 UI 层没人消费。)
四、失败路径与日志
4.1 setActive 返回 false 的场景
沿代码路径枚举:
- 三段全被门卫拦下(最常见):设备未连接对应 profile(
isConnectedProfile非STATE_CONNECTED,含 CONNECTING 中)、或该 flavor 未创建此 profile 对象 →result保持 false。无任何日志。 mBluetoothAdapter == null:profile 构造时getAdapterByProfile返回 null。构造已final赋值,正常不命中;命中即 framework 侧路由异常。无日志,静默 false。- framework 拒绝:
mBluetoothAdapter.setActiveDevice(...)binder 返回 false(栈内部校验失败、断连竞态等)。本仓侧无日志,要去系统日志找。 - 注意一个时序窗口:因为 setActiveDevice 不依赖 mService,profile proxy 未就绪不会导致 false(这与 AOSP 原版行为不同);但门卫链
isConnectedProfile→getProfileConnectionState(dcddif CachedBluetoothDevice.java:488-491)→profile.getConnectionStatus(mDevice),而各 profile 的getConnectionStatus在mService == null时直接返回STATE_DISCONNECTED(如dcddif A2dpProfile.java:159-164)——即 proxy 刚 bind、mService 还没就绪的那一瞬间,门卫必然把请求拦掉,且无日志。
4.2 UI 反馈
- 由于
setActive()无 UI 调用方,setActive 的失败没有任何 UI 反馈路径。 - UI 实际的”连接失败”反馈在列表项层:
MiCarBluetoothBondedDevicePreference.kt:78的R.string.bluetooth_start_connect_device_failed(连接发起失败的 toast/文案)。
4.3 排障日志 tag 速查(均为 android.util.Log,非 MLog)
| tag | 所在类 | 关键日志 |
|---|---|---|
CachedBluetoothDevice | dcddif CBD:104 / xcddif CBD:71 | OnPreferenceClickListener: A2DP/Headset/Hearing Aid/LeAudio active device=(仅 setActive 成功段);onActiveDeviceChanged: unknown profile(w) |
BluetoothEventManager | dcddif BEM:51 | ActiveDeviceChangedHandler: action is null / unknown action(w) |
A2dpProfile / HeadsetProfile / HearingAidProfile / LeAudioProfile | 各文件头部(如 dcddif A2dpProfile.java:42) | 本篇链路上基本无输出(setActiveDevice 无日志) |
实操结论:排 active 问题别在设置侧 UI 日志里大海捞针——设置侧可观测信号只有 CachedBluetoothDevice tag 的成功 log 和 BluetoothEventManager 的异常 log,主战场在系统侧蓝牙栈日志。
五、设置成功的回环(一句话衔接)
setActiveDevice 受理后,系统广播 ACTION_ACTIVE_DEVICE_CHANGED(A2DP/HFP/HA 三种,xcddif 另有 ACTION_LE_AUDIO_ACTIVE_DEVICE_CHANGED,注册见 dcddif BluetoothEventManager.java:114-117 / xcddif BluetoothEventManager.java:136-141)→ ActiveDeviceChangedHandler(dcddif :431-453 / xcddif :706-731)→ dispatchActiveDeviceChanged(dcddif :225-234 / xcddif :310-336,xcddif 多一段 CSIP 组成员设备映射到主设备的逻辑)→ 每台 cached 设备的 onActiveDeviceChanged() 更新 mIsActiveDeviceXxx 布尔位 → dispatchAttributesChanged() 通知 UI。该回环的完整拆解见 72/74 章,本篇不展开。
六、flavor 对照表
先重申:本仓只有 dcddif / xcddif 两份 settingslib 蓝牙源集,无 f3dif。
| 维度 | dcddif | xcddif |
|---|---|---|
| setActive() 位置 | CachedBluetoothDevice.java:544-568 | CachedBluetoothDevice.java:730-761 |
| profile 段数 | 3(A2DP/Headset/HearingAid) | 4(+LeAudio,:753-759) |
| 门卫条件 | profile != null && isConnectedProfile(每段同构) | 同左 |
| 返回值 | 任一段受理即 true(OR 语义) | 同左 |
| 成功日志前缀 | OnPreferenceClickListener: ... | 同左(多 LeAudio 变体) |
| active 广播注册 | BEM:114-117(3 种 action) | BEM:136-141(4 种,+LE_AUDIO) |
| onActiveDeviceChanged case 数 | 3(CBD:640-663) | 4(CBD:835+,含 LE_AUDIO :856-858,字段 :129) |
| dispatchActiveDeviceChanged | BEM:225-234 直分发 | BEM:310-336,多 CSIP 成员→主设备映射(:316-330)与 mDeviceManager.onActiveDeviceChanged(:331) |
| MICAR-PORTING 标记(CBD 内) | 10 处 | 33 处 |
| MIUI 标记(CBD 内) | 0 处 | 9 处(如 MIUI ADD: BT_LeAudio :546/:1777,MIUI ADD: BT_BluetoothHalfClose :1339 等,均在 setActive 链路之外的半关/LeAudio 功能区) |
| setActiveDevice 实现差异 | A2dp:166-173 / Headset:127-135 / HA:166-176 | 逐字相同(行号平移)+ LeAudio:165-172 |
七、与双蓝牙(BluetoothAdapterUtil / mActiveAdapter)的交集
结论:setActive 链与双蓝牙强交集——它本身就是双蓝牙改造的一部分。 具体三层证据:
- profile 层必经路由:A2dp/Headset/HearingAid 三个 profile 的
setActiveDevice全部经mBluetoothAdapter(构造时BluetoothAdapterUtil.getAdapterByProfile(getProfileId())选定的适配器,MICAR-PORTING 标记),不是 AOSP 原版的mService直调。即”这条激活命令发到哪个蓝牙适配器的栈”由 framework 的 AdapterUtil 路由决定。唯一例外是 LeAudio(仅 xcddif):构造用BluetoothAdapter.getDefaultAdapter()(xcddif LeAudioProfile.java:114),不走路由。 device == null分支的 removeActiveDevice 同样经 mBluetoothAdapter,取消激活也走同一适配器。- CachedBluetoothDevice 层的 mActiveAdapter:
dcddif CBD:116(private BluetoothAdapter mActiveAdapter;)、dcddif CBD:120(mActiveDevice字段)、xcddif CBD:89。connect/disconnect 分别走mActiveAdapter.connectAllEnabledProfiles(dcddif CBD:402)/disconnectAllEnabledProfiles(dcddif CBD:325)。注意setActive()方法体本身(544-568/730-761)没有引用 mActiveAdapter/mActiveDevice——它只经 profile 层的 mBluetoothAdapter 间接关联双蓝牙;两 flavor 的适配器选择时机不同(dcddif CBD:445-447 在配对流程getDefaultAdapter()/getNewAdapter(),xcddif CBD:601-606getNewAdapter(mDevice)按设备选)。
另注:BluetoothAdapterUtil 是 framework 类(android.bluetooth.BluetoothAdapterUtil),其源码不在本仓,路由细节(profile id → 适配器实例的映射规则)超出本仓视野。
八、排障要点
- “点了没反应”先分岔:车机 UI 没有”设为活跃”按钮,用户说的”切不过去”几乎都是连接层问题。先查
CachedBluetoothDevice/MiBluetoothExt 的 MLog(connect device: ...),再看系统栈日志里 connect 后 active 是否流转。 - 搜成功信号:
adb logcat -s CachedBluetoothDevice找OnPreferenceClickListener: ... active device=。搜不到不代表没发起——被门卫拦下(未连接)时无日志,要用adb dumpsys bluetooth_manager看连接状态反推。 - 广播断点:
BluetoothEventManagertag 下unknown action/action is null说明收到了变形的 active 广播(或注册表外的 action),对照第六节注册表核对。 - 双蓝牙疑案:两台适配器场景下 active 异常时,记住 A2DP/HFP/HA 的 setActiveDevice 走
getAdapterByProfile路由、LeAudio 走默认适配器——不对称点就在这,需要系统侧日志确认路由结果。 - CONNECTING 陷阱:
isConnectedProfile只认STATE_CONNECTED,连接还在建立中时任何 active 请求都会被门卫静默拦掉(如果未来有代码启用 setActive)。
自测清单
- 能说出 setActive() 三道结构:null 门卫、isConnectedProfile 门卫、OR 语义返回
- 能解释为什么三段 profile 要分别 setActiveDevice(媒体/通话通道独立 active)
- 能说出 setActiveDevice 返回 true 的语义是”命令被受理”而非”已生效”,生效标志是 ACTION_ACTIVE_DEVICE_CHANGED
- 能说出本仓 setActive() 零调用这一事实,以及车机”切换设备”的交互等价物(连接新设备 + 满员时 Picker 替换)
- 能徒手还原:handlePreferenceChanged:144 → connectDevice:115 → performConnect(MiBluetoothExt.kt:16) → connect():359 → connectAllEnabledProfiles:402
- 能说出 LeAudio 的两个例外:仅 xcddif 有、构造用 getDefaultAdapter 不走双蓝牙路由
- 知道排障日志 tag:CachedBluetoothDevice / BluetoothEventManager,且设置侧失败路径基本无日志
- 能说出本仓无 f3dif,带 LeAudio 第四段的是 xcddif
交叉引用
- 67 · active 设备与多设备——布尔位语义与多设备模型
- 64 · 连接与断开——performConnect 背后的 connect/disconnect 全链
- 70 · 总览与闭环地图——本系列的闭环地图
- 72 · 监听侧 / 74 · 消费侧——第五节回环的完整拆解