73 · 设置侧 — setActive 与车机 UI 触发链

本篇讲清什么

上一篇(67 章)讲了”什么是 active 设备、mIsActiveDeviceA2dp 这些布尔位怎么用”。本篇往下钻一层,回答三个问题:

  1. 设置侧怎么把一台设备设为 active?——逐行精讲 CachedBluetoothDevice.setActive() 和它下面四个 profile 的 setActiveDevice()
  2. 车机 UI 上用户做什么操作会走到这条链?——这是本篇最有”车机特色”的部分,结论可能反直觉:本仓 UI 从不直接调用 setActive(),车机上”切音源”的交互等价物是”连接新设备(必要时先断旧设备)“。我们把完整 UI 链路还原出来。
  3. 失败了怎么办、去哪看日志?

阅读前置:需要知道 64 章的连接链路(本篇会反复对照),以及”profile”是指 A2DP/HFP 等蓝牙规格在设置侧的包装类。

一个先说清的事实:本仓 base/settingsLibAndroid/src 下只有 dcddifxcddif 两个 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 分支更新 mIsActiveDeviceLeAudioxcddif 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>);
    }

三个值得停下来看的点:

  1. 判空的是 mBluetoothAdapter,不是 mService 这和 AOSP 原版(走 mService.setActiveDevice(device) 即 BluetoothA2dp proxy)不同,是 MiCar 的双蓝牙改造(// MICAR-PORTING-START 标记)。构造时 mBluetoothAdapter = BluetoothAdapterUtil.getAdapterByProfile(getProfileId()) 就已赋值,字段还是 private finaldcddif A2dpProfile.java:50),所以这个判空实际是防御式写法,正常运行几乎不会命中。也正因为不走 mService,profile proxy 尚未 bind 完成时 setActiveDevice 依然能发出去——这是与 getActiveDevice()(走 mService,null 时直接返回 null)的一个行为差异。
  2. device == null 的分支是”取消激活”:调 removeActiveDevice(type) 把当前 active 清掉(传 null 给本方法即”不再指定任何 active 设备”)。
  3. 跨进程边界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-END
    BluetoothAdapterUtilframework 类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 判 mServicedcddif 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 一张表

profileflavor位置profile type构造路由MICAR-PORTING 标记
A2dpProfiledcddifA2dpProfile.java:166-173ACTIVE_DEVICE_AUDIOgetAdapterByProfile(:105-107)有(2 处)
A2dpProfilexcddifA2dpProfile.java:171-178ACTIVE_DEVICE_AUDIOgetAdapterByProfile(:110-112)有(4 处)
HeadsetProfiledcddifHeadsetProfile.java:127-135ACTIVE_DEVICE_PHONE_CALLgetAdapterByProfile(:105-107)有(2 处)
HeadsetProfilexcddifHeadsetProfile.java:127-135ACTIVE_DEVICE_PHONE_CALLgetAdapterByProfile(:105-107)有(4 处)
HearingAidProfiledcddifHearingAidProfile.java:166-176通话中 PHONE_CALL,否则 AUDIOgetAdapterByProfile(:106)有(4 处)
HearingAidProfilexcddifHearingAidProfile.java:200-210通话中 PHONE_CALL,否则 AUDIOgetAdapterByProfile(:140)有(4 处)
LeAudioProfile仅 xcddifLeAudioProfile.java:165-172ACTIVE_DEVICE_ALLgetDefaultAdapter(: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),点选后 onClickdoConnect(...)(: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 的场景

沿代码路径枚举:

  1. 三段全被门卫拦下(最常见):设备未连接对应 profile(isConnectedProfileSTATE_CONNECTED,含 CONNECTING 中)、或该 flavor 未创建此 profile 对象 → result 保持 false。无任何日志
  2. mBluetoothAdapter == null:profile 构造时 getAdapterByProfile 返回 null。构造已 final 赋值,正常不命中;命中即 framework 侧路由异常。无日志,静默 false
  3. framework 拒绝mBluetoothAdapter.setActiveDevice(...) binder 返回 false(栈内部校验失败、断连竞态等)。本仓侧无日志,要去系统日志找。
  4. 注意一个时序窗口:因为 setActiveDevice 不依赖 mService,profile proxy 未就绪不会导致 false(这与 AOSP 原版行为不同);但门卫链 isConnectedProfilegetProfileConnectionStatedcddif CachedBluetoothDevice.java:488-491)→ profile.getConnectionStatus(mDevice),而各 profile 的 getConnectionStatusmService == null直接返回 STATE_DISCONNECTED(如 dcddif A2dpProfile.java:159-164)——即 proxy 刚 bind、mService 还没就绪的那一瞬间,门卫必然把请求拦掉,且无日志。

4.2 UI 反馈

  • 由于 setActive() 无 UI 调用方,setActive 的失败没有任何 UI 反馈路径
  • UI 实际的”连接失败”反馈在列表项层:MiCarBluetoothBondedDevicePreference.kt:78R.string.bluetooth_start_connect_device_failed(连接发起失败的 toast/文案)。

4.3 排障日志 tag 速查(均为 android.util.Log,非 MLog)

tag所在类关键日志
CachedBluetoothDevicedcddif CBD:104 / xcddif CBD:71OnPreferenceClickListener: A2DP/Headset/Hearing Aid/LeAudio active device=(仅 setActive 成功段);onActiveDeviceChanged: unknown profile(w)
BluetoothEventManagerdcddif BEM:51ActiveDeviceChangedHandler: 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。

维度dcddifxcddif
setActive() 位置CachedBluetoothDevice.java:544-568CachedBluetoothDevice.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)
dispatchActiveDeviceChangedBEM: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 链与双蓝牙强交集——它本身就是双蓝牙改造的一部分。 具体三层证据:

  1. profile 层必经路由:A2dp/Headset/HearingAid 三个 profile 的 setActiveDevice 全部经 mBluetoothAdapter(构造时 BluetoothAdapterUtil.getAdapterByProfile(getProfileId()) 选定的适配器,MICAR-PORTING 标记),不是 AOSP 原版的 mService 直调。即”这条激活命令发到哪个蓝牙适配器的栈”由 framework 的 AdapterUtil 路由决定。唯一例外是 LeAudio(仅 xcddif):构造用 BluetoothAdapter.getDefaultAdapter()xcddif LeAudioProfile.java:114),不走路由。
  2. device == null 分支的 removeActiveDevice 同样经 mBluetoothAdapter,取消激活也走同一适配器。
  3. CachedBluetoothDevice 层的 mActiveAdapterdcddif CBD:116private BluetoothAdapter mActiveAdapter;)、dcddif CBD:120mActiveDevice 字段)、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-606 getNewAdapter(mDevice) 按设备选)。

另注:BluetoothAdapterUtil 是 framework 类(android.bluetooth.BluetoothAdapterUtil),其源码不在本仓,路由细节(profile id → 适配器实例的映射规则)超出本仓视野。


八、排障要点

  1. “点了没反应”先分岔:车机 UI 没有”设为活跃”按钮,用户说的”切不过去”几乎都是连接层问题。先查 CachedBluetoothDevice/MiBluetoothExt 的 MLog(connect device: ...),再看系统栈日志里 connect 后 active 是否流转。
  2. 搜成功信号adb logcat -s CachedBluetoothDeviceOnPreferenceClickListener: ... active device=。搜不到不代表没发起——被门卫拦下(未连接)时无日志,要用 adb dumpsys bluetooth_manager 看连接状态反推。
  3. 广播断点BluetoothEventManager tag 下 unknown action/action is null 说明收到了变形的 active 广播(或注册表外的 action),对照第六节注册表核对。
  4. 双蓝牙疑案:两台适配器场景下 active 异常时,记住 A2DP/HFP/HA 的 setActiveDevice 走 getAdapterByProfile 路由、LeAudio 走默认适配器——不对称点就在这,需要系统侧日志确认路由结果。
  5. 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

交叉引用