03 · 蓝牙设备详情与开关状态控制
核心文件:
view/MiCarBluetoothDeviceDetailFragment.java、controller/BluetoothSwitchController.java、controller/BluetoothDeviceProfilesPreferenceController.java、controller/BluetoothBondedDevicesPrefController.java
0. 关键结论先览
- MiCar 实际用
micar_bluetooth_device_details_fragment.xml(MiCarBluetoothDeviceDetailFragment.java:154)。同目录bluetooth_device_details_fragment.xml是 AOSP 遗产(含重命名/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/onRightTextClicked 走 handlePreferClick(连接/断开),不进详情页。
Fragment 装配(MiCarBluetoothDeviceDetailFragment.java)
| 生命周期 | 行号 | 关键动作 |
|---|---|---|
newInstance(device) | :50-56 | BluetoothDevice(Parcelable) 塞 args KEY_DEVICE_ADDRESS |
onCreate | :73-79 | 缓存 controller 列表中所有 MisComplexSdk.IComplexConnectListener;LocalBluetoothManager.findDevice() 取 CachedBluetoothDevice |
getPreferenceScreenResId | :153-155 | R.xml.micar_bluetooth_device_details_fragment |
onViewCreated | :82-129 | 标题改设备名+返回箭头;注册 MisComplexSdk 复合连接监听,view.post 分发子 controller |
onResume | :138-149 | 防御:mCachedDevice==null 或 BOND_NONE 直接 popBackStack(处理配置变更重建后设备已解配) |
updateCachedBluetoothDevice | :157-162 | device 注入 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)
isShow = isPhoneDevice && !isTwoWayRadio(手机类、非对讲机)- isShow=true 且 profile ready →
setEnabled(true)+setChecked;profile 未 ready → 变灰但可见 - 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-152 才 setCachedDevice(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-103:setPositiveBtnEnable(!isBusy())(:99)无防抖、无 profile 过滤直驱;经 BluetoothDeviceOperationPreference.setPositiveBtnEnable:54 → SwitchLoadingHelper.setLoading(MINIMUM_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:
- 关蓝牙且有 Mirror/AndroidAuto 复合连接 → 弹二次确认(:151-174),取消则
return false回滚 setLoading(true)(:176)mBluetoothAdapter.enable()/disable()(:179-184)mUIBluetoothSwitchState = SWITCHING(:185)clearDisableAutoConnect()(:186)
收敛 handleStateChanged:195-250:
| adapter state | UI 动作 |
|---|---|
| STATE_TURNING_ON | setLoading(true),未 checked 则 setChecked(true) |
| STATE_ON | syncDeviceName、setEnabled、setLoading(false)、setChecked(true)、强制 scanMode=CONNECTABLE_DISCOVERABLE(:226-232,保证车机一直可被发现) |
| STATE_TURNING_OFF | setLoading(true)、setChecked(false) |
| STATE_OFF | setLoading(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;已连接→disconnectMis 或 showDisConnectConfirmDialog。connectDevice:245-301 三层判断:非手机类直连 / 已连 < 2 直连 / 否则弹 Picker 冲替。
5. 连接主页蓝牙入口(BluetoothStateController)
XML connection_settings_fragment.xml:30-37,职责单一:显示蓝牙当前开关状态文本(右侧小字),不含开关控件(开关在蓝牙二级页)。
onCreateInternal:37-43注册ACTION_STATE_CHANGEDReceiverupdateTitle(turnOn):67-71右侧文字切换micar_bluetooth_turned_on/offonResumeInternal:83-86回前台同步- 点击完全由 XML
<intent>决定:BLUETOOTH_SETTINGS→BluetoothSettingsActivity,Controller 不处理点击
6. 操作集合
| 操作 | 详情页/主列表 | 入口 |
|---|---|---|
| 断开 | ✓ 两者 | BluetoothOperationsController:160-178 / Bonded:220-243 → performDisConnect(MiBluetoothExt.kt:32-48):disconnect + 发 com.android.micar.settings.DEVICE_DISCONNECT 广播 |
| 取消配对 | ✓ 详情页 | BluetoothOperationsController:179-197 / onCreateDialog:165-191 → performUnpair,先 setLastUnboundDevice 防立即被扫回(Unbonded:306-319 拦截一次) |
| 连接 | ✓ 详情页 | BluetoothOperationsController:148-159 → connectDevice(三层判断) |
| 重命名 | ❌ 不存在 | AOSP 遗产有 BluetoothDeviceNamePreferenceController,MiCar 不加载 |
| 设为默认 | ❌ 不存在 | 无 “default device” 概念,只有”最近连接设备”(recordLastConnectedDevice,供蓝牙音乐 app 用) |
7. 设计要点与坑
- Fragment 栈:进入 launchFragment 压栈;退出三路径——返回键(
BluetoothSettingsActivity.onBackPressed:66-75)、BOND_NONE→goBack(Operations:84-89)、onResume 设备失效→popBackStack(:142-147)。resetToMainFragmentIfNeeded():106-121在 onNewIntent/STATE_OFF 时强制回主页。 - MIS 联动:详情页
MisComplexSdk.IComplexConnectListener(:94-128)fan-out 到子 controller;操作前经confirmConnectWhenMisExist/confirmDisconnectWhenMisConnected/confirmDeleteDeviceWhenMisExist确认是否冲掉 Mirror/AA/CarPlay。 - Profile 切换异步:唯一回滚靠 framework(onPreferenceChange 返回 false),无手动 retry/loading。
- 竞速坑:① UI 先行若 adapter enable 失败,loading 永不消(补救靠 onStartInternal IDLE 时重刷,但 IDLE 仅广播到来才置位);② 关蓝牙瞬间 3-4 路 UI 收敛(Switch/State/MainEntry/Bonded Controller),故需
preHandleBtOff提前隐藏;③mIsControllerActive双检防后台任务操作已销毁 UI;④ 详情页mCachedDevice不监听删除,依赖 OperationsControllermDeviceCallback触发 goBack(隐式依赖);⑤ 解配设备”一次性拦截”防 UX 混乱;⑥ 基类回调注册时序缺陷(4867):BluetoothDevicePreferenceController在 onStart 注册设备回调但彼时 mCachedDevice==null,onResume 才注入——子类若不在 setCachedDevice 自注册(如 BluetoothOperationsController:55 那样做),设备级回调永远不送达。