03 · 蓝牙设备详情与开关状态控制

核心文件:view/MiCarBluetoothDeviceDetailFragment.javacontroller/BluetoothSwitchController.javacontroller/BluetoothDeviceProfilesPreferenceController.javacontroller/BluetoothBondedDevicesPrefController.java

0. 关键结论先览

  • MiCar 实际用 micar_bluetooth_device_details_fragment.xmlMiCarBluetoothDeviceDetailFragment.java:154)。同目录 bluetooth_device_details_fragment.xmlAOSP 遗产(含重命名/MAC/profiles 容器),MiCar 不加载
  • MiCar 详情页只暴露 1 个 profile 开关:PBAP(电话本访问),手机可见、对讲机隐藏;HFP/A2DP/PBAP-Server 在 CachedBluetoothDevice.connect() 内部自动连,UI 无独立开关。
  • 详情页极简:标题=设备名 + PBAP 开关 + 底部「连接/断开」「取消配对」。没有重命名、没有设为默认、没有 MAC 展示(重命名/默认设备在 MiCar 蓝牙 UI 整体不存在)。
  • 蓝牙开关「UI 先行」:点击 → setLoading(true)+setChecked(newValue) 立即推 UI → 真正 enable/disable → 收广播后终态收敛。
  • mUIBluetoothSwitchState(IDLE/SWITCHING)做”页面回前台防抖”。

1. 设备详情页架构

进入路径(唯一入口)

已配对列表项点「右侧箭头」:BluetoothBondedDevicesPrefController.java:151-153

getFragmentController().launchFragment(
    MiCarBluetoothDeviceDetailFragment.newInstance(pref.getCachedDevice()));

onPreferenceClicked/onRightTextClickedhandlePreferClick(连接/断开),不进详情页

Fragment 装配(MiCarBluetoothDeviceDetailFragment.java

生命周期行号关键动作
newInstance(device):50-56BluetoothDevice(Parcelable) 塞 args KEY_DEVICE_ADDRESS
onCreate:73-79缓存 controller 列表中所有 MisComplexSdk.IComplexConnectListenerLocalBluetoothManager.findDevice() 取 CachedBluetoothDevice
getPreferenceScreenResId:153-155R.xml.micar_bluetooth_device_details_fragment
onViewCreated:82-129标题改设备名+返回箭头;注册 MisComplexSdk 复合连接监听,view.post 分发子 controller
onResume:138-149防御mCachedDevice==nullBOND_NONE 直接 popBackStack(处理配置变更重建后设备已解配)
updateCachedBluetoothDevice:157-162device 注入 ProfilesController + OperationsController,refresh=true
onCreateDialog(DIALOG_DEVICE_UNPAIR):165-191取消配对弹窗;确认后 setLastUnboundDevice+performUnpair

详情页 XML(micar_bluetooth_device_details_fragment.xml)仅 4 元素:顶部留白 / MiCarConnectSwitchPreference(pk_bluetooth_pbap_profile) / 中部留白 / BluetoothDeviceOperationPreference(pk_bluetooth_device_operations)。

flowchart TD
    A["已配对设备 item<br/>MiCarBluetoothBondedDevicePreference"] -- "点右侧箭头<br/>Bonded:151" --> B
    subgraph 详情页["MiCarBluetoothDeviceDetailFragment"]
        T["标题=device.getName() :88-92"]
        T --> P["PBAP 开关<br/>BluetoothDeviceProfilesPreferenceController"]
        P --> O["操作按钮<br/>BluetoothOperationsController"]
    end
    O -- "Connect/DisConnect/Unpair" --> C{"PreferenceAction"}
    C -->|Connect| C1["performConnect+MIS确认 :148"]
    C -->|DisConnect| C2["performDisConnect+回退 :160"]
    C -->|Unpair| C3["DIALOG_DEVICE_UNPAIR :179→performUnpair"]
    C3 --> POP["goBack 回主列表"]
    P -- "handlePreferenceChanged" --> PBAP["PbapClientProfile.setEnabled :93-99"]

2. Profile 管理

「列表动态生成」不存在——退化成单个 PBAP 开关

AOSP 原版按 getProfiles() 动态铺若干开关,MiCar 重写 BluetoothDeviceProfilesPreferenceController

  • preference 类型钉死成 MiCarConnectSwitchPreference(:37-38,59-61),非 PreferenceGroup
  • onCreateInternal :48-56 取 PbapClientProfile 引用

PBAP 可见性(updateState :64-87)

  1. isShow = isPhoneDevice && !isTwoWayRadio(手机类、非对讲机)
  2. isShow=true 且 profile ready → setEnabled(true)+setChecked;profile 未 ready → 变灰但可见
  3. isShow=false → setVisible(false),空占位显示

切换链路

handlePreferenceChanged:89-102 直接 return mPbapClientProfile.setEnabled(device, newValue)。返回 false 时 framework 自动回滚 switch——唯一回滚机制,无 retry/超时/loading,失败用户只看到开关”弹回去”。

⚠️ 订正(2026-09-09,MANNPROB-4867 实证):上图”PM→CDC→PC 回调收敛”链实际不工作——基类 BluetoothDevicePreferenceController.onStartInternal:92-97 仅在 mCachedDevice!=null 时注册 CachedBluetoothDevice.Callback,而 Fragment onResume:141-152setCachedDevice(onStart 先于 onResume),开关行 refreshUi 回调从未注册。4867 bugreport 实证:PBAP 断开窗口内开关行零 updateState、零 getConnectionPolicy(Settings 侧)。开关不抖是缺陷的偶然保护,不是设计;弹窗开着时手机侧改电话本权限开关将不同步。修复:setCachedDevice 补注册或 onStart 幂等重注册(updateState 读 policy 单调,不会引入抖动)。

sequenceDiagram
    participant U as 用户
    participant P as MiCarConnectSwitchPreference
    participant PC as BluetoothDeviceProfilesPreferenceController
    participant PM as PbapClientProfile
    participant CDC as CachedBluetoothDevice.Callback
    U->>P: 拨开关 ON
    P->>PC: handlePreferenceChanged(true)
    PC->>PM: setEnabled(device, true)
    Note over PM,CDC: setEnabled→setConnectionPolicy→状态广播<br/>→LBPM:449 refresh()→dispatchAttributesChanged
    PM--xCDC: onDeviceAttributesChanged(基类注册缺陷,4867 实证未送达)
    Note over P: setEnabled 返回 false 时 framework 回滚 UI

「断开连接」按钮的置灰驱动链(4867 定案)

BluetoothOperationsController.updateConnectionButtonState:82-103setPositiveBtnEnable(!isBusy())(:99)无防抖、无 profile 过滤直驱;经 BluetoothDeviceOperationPreference.setPositiveBtnEnable:54SwitchLoadingHelper.setLoadingMINIMUM_LOADING_TIME=600ms 只保最短展示,不构成防抖)→ onBindViewHolder mPositiveBtn.setEnabled(!isLoading()) 置灰。isBusy()(f3dif CBD:1042-1053)遍历 mProfiles 逐 profile 活 binder getConnectionStatus任一 profile CONNECTING/DISCONNECTING 即 true——关 PBAP 开关触发的 PBAP DISCONNECTING 窗(0.36~0.93s,下载中 abort 更长)全程按钮置灰 = 4867「断开连接闪一下」。修复方向:busy 判定排除 PBAP 瞬态(保留 HFP/A2DP 拆链置灰的正向反馈)。另::100 setButtonAction(isConnected()) 无条件 notifyChanged,单次 dispatch 按钮行 ≥2 次重绑(卫生项)。

同节注意:本 Controller 的回调是自己在 setCachedDevice:46-57 里注册的(重写方法),所以它能收到 PBAP 断开通知——与上面 PBAP 开关行(基类注册、从未生效)形成同页不对称。

其他 profile(HFP/A2DP/PBAP-Server)何时连?

无独立开关。点「连接」时 CachedBluetoothDevice.connect()MiBluetoothExt.kt:16-24 performConnect)把设备支持的所有 profile 一并连。MiCar 额外做:发 ACTION_CONNECT_NEW_DEVICE 广播让音乐 app 暂停(MiBluetoothUtils.kt:369-381)+ 异步 appendAction。

3. 蓝牙开关状态机(BluetoothSwitchController)

双轨状态:adapter 真实状态(ACTION_STATE_CHANGED 广播,:45-62)+ UI 状态机 mUIBluetoothSwitchState(IDLE=0/SWITCHING=1,:48-51)。

用户点击 onPreferenceChange:142-189

  1. 关蓝牙且有 Mirror/AndroidAuto 复合连接 → 弹二次确认(:151-174),取消则 return false 回滚
  2. setLoading(true)(:176)
  3. mBluetoothAdapter.enable()/disable()(:179-184)
  4. mUIBluetoothSwitchState = SWITCHING(:185)
  5. clearDisableAutoConnect()(:186)

收敛 handleStateChanged:195-250

adapter stateUI 动作
STATE_TURNING_ONsetLoading(true),未 checked 则 setChecked(true)
STATE_ONsyncDeviceName、setEnabled、setLoading(false)、setChecked(true)、强制 scanMode=CONNECTABLE_DISCOVERABLE(:226-232,保证车机一直可被发现)
STATE_TURNING_OFFsetLoading(true)、setChecked(false)
STATE_OFFsetLoading(false)、setChecked(false)

STATE_ON/OFF 复位 mUIBluetoothSwitchState=IDLE(:204-206)。

防抖(onStartInternal:105-118):IDLE 时用 adapter state 重刷(安全);SWITCHING 时不刷(注释 :112 “防止 UI 跳变”)。

stateDiagram-v2
    direction LR
    [*] --> OFF
    OFF --> TURNING_ON : onPreferenceChange(true)<br/>setLoading+adapter.enable()
    TURNING_ON --> ON : STATE_ON 广播<br/>重置 scanMode
    ON --> TURNING_OFF : onPreferenceChange(false)<br/>可能 MIS 确认+preHandleBtOff
    TURNING_OFF --> OFF : STATE_OFF 广播
    note right of TURNING_ON: mUIBluetoothSwitchState=SWITCHING<br/>onStart 回前台不重刷 UI
    note left of OFF: preHandleBtOff 先隐藏列表

副状态:运输模式(Driving.CAR_MODE_STATUS==TRANSPORT)禁用(:76-85,220,243);DISALLOW_BLUETOOTH 用户限制(:259-261);关蓝牙前先 preHandleBtOff()(:191-193)提前隐藏已配对/未配对列表防闪烁。

4. 已配对列表 Controller(BluetoothBondedDevicesPrefController)

XML key=pk_bluetooth_paired_devices,继承 BluetoothPreferenceController<PreferenceGroup>(实现 BluetoothCallback)。

刷新两路

  • A. 全量 updateState→innerUpdateState:303-372:首次 200ms 延时(BLUETOOTH_DATA_LAZY_TIME),后台线程取 getCachedDevicesCopy() 过滤 bondedDeviceFilter(BOND_BONDED),切主线程 removeAll+重建。双重 mIsControllerActive 检查(:328,349,AI 标注代码防竞态)。仅 adapter.isEnabled() 才填充(:358)。
  • B. 增量回调:onDeviceAdded:408/onDeviceDeleted:421/onDeviceBondStateChanged:486/onBluetoothStateChanged:467(STATE_ON→refresh,OFF/TURNING_OFF→清空+隐藏)/onProfileConnectionStateChanged:439(局部 refreshDeviceUi)/onDeviceStateChanged:374(延迟 200ms 重排序+滚顶)。

子 Preference(createDevicePreference:140-182):MiCarBluetoothBondedDevicePreference + 右箭头 micar_ic_view_detail_indicator + BasicPreferenceOnClickListener(onRightIconClicked→详情页;onPreferenceClicked/onRightTextClicked→handlePreferClick 连接/断开)。

连接/断开(handlePreferClick:166-179):未连接→connectMis(MIS 确认)或 connectDevice;已连接→disconnectMisshowDisConnectConfirmDialogconnectDevice:245-301 三层判断:非手机类直连 / 已连 < 2 直连 / 否则弹 Picker 冲替。

5. 连接主页蓝牙入口(BluetoothStateController)

XML connection_settings_fragment.xml:30-37职责单一:显示蓝牙当前开关状态文本(右侧小字),不含开关控件(开关在蓝牙二级页)。

  • onCreateInternal:37-43 注册 ACTION_STATE_CHANGED Receiver
  • updateTitle(turnOn):67-71 右侧文字切换 micar_bluetooth_turned_on/off
  • onResumeInternal:83-86 回前台同步
  • 点击完全由 XML <intent> 决定BLUETOOTH_SETTINGSBluetoothSettingsActivity,Controller 不处理点击

6. 操作集合

操作详情页/主列表入口
断开✓ 两者BluetoothOperationsController:160-178 / Bonded:220-243performDisConnect(MiBluetoothExt.kt:32-48):disconnect + 发 com.android.micar.settings.DEVICE_DISCONNECT 广播
取消配对✓ 详情页BluetoothOperationsController:179-197 / onCreateDialog:165-191performUnpair,先 setLastUnboundDevice 防立即被扫回(Unbonded:306-319 拦截一次)
连接✓ 详情页BluetoothOperationsController:148-159 → connectDevice(三层判断)
重命名❌ 不存在AOSP 遗产有 BluetoothDeviceNamePreferenceController,MiCar 不加载
设为默认❌ 不存在无 “default device” 概念,只有”最近连接设备”(recordLastConnectedDevice,供蓝牙音乐 app 用)

7. 设计要点与坑

  1. Fragment 栈:进入 launchFragment 压栈;退出三路径——返回键(BluetoothSettingsActivity.onBackPressed:66-75)、BOND_NONE→goBack(Operations:84-89)、onResume 设备失效→popBackStack(:142-147)。resetToMainFragmentIfNeeded():106-121 在 onNewIntent/STATE_OFF 时强制回主页。
  2. MIS 联动:详情页 MisComplexSdk.IComplexConnectListener(:94-128)fan-out 到子 controller;操作前经 confirmConnectWhenMisExist/confirmDisconnectWhenMisConnected/confirmDeleteDeviceWhenMisExist 确认是否冲掉 Mirror/AA/CarPlay。
  3. Profile 切换异步:唯一回滚靠 framework(onPreferenceChange 返回 false),无手动 retry/loading。
  4. 竞速坑:① UI 先行若 adapter enable 失败,loading 永不消(补救靠 onStartInternal IDLE 时重刷,但 IDLE 仅广播到来才置位);② 关蓝牙瞬间 3-4 路 UI 收敛(Switch/State/MainEntry/Bonded Controller),故需 preHandleBtOff 提前隐藏;③ mIsControllerActive 双检防后台任务操作已销毁 UI;④ 详情页 mCachedDevice 不监听删除,依赖 OperationsController mDeviceCallback 触发 goBack(隐式依赖);⑤ 解配设备”一次性拦截”防 UX 混乱;⑥ 基类回调注册时序缺陷(4867):BluetoothDevicePreferenceController 在 onStart 注册设备回调但彼时 mCachedDevice==null,onResume 才注入——子类若不在 setCachedDevice 自注册(如 BluetoothOperationsController:55 那样做),设备级回调永远不送达。