dev vs dev_a17_0610 蓝牙适配差异详解(底层代码级)
面向:蓝牙底层适配工程师 目的:逐处讲清两版蓝牙代码在 API / 字段 / 逻辑 上的具体差异,供评估跨版本/跨平台适配工作 产出:2026-07-24 | 根因 = Adapter 类型体系不同(第 1 节,必读) 配套:决策视角见 dev-vs-dev_a17_0610蓝牙差异报告
0. 怎么读这份文档
版本约定:
- a17 =
dev_a17_0610分支(Android 17 工具链,编译 flavorxcd_cn_a17/xcd_global_a17,settingslib 走 f3dif) - dev =
dev分支(编译 flavordcd/xcd,settingslib 走 dcddif/xcddif) - 当前产线分支
rls-xcd-u-mp-26-v6.0≈ dev 风格
代码块标注:每个差异点给出 a17 实现 与 dev 实现 两段真实代码(来自 git diff dev_a17_0610 dev,-行=a17,+行=dev)。
一句话总览:a17 用标准 BluetoothAdapter + 实时系统查询 + AOSP 对齐;dev 用 settingslib 的 LocalBluetoothAdapter 包装类 + 缓存状态 + 自有扫描 API。所有差异都源于这个根因。
1. 🔑 根因:Adapter 类型体系差异(必读)
这是全部分歧的源头。MiBluetoothUtils.getAdapter() 的返回类型不同,导致所有调用方写法不同。
文件:settingsPage/micarConnectionSettings/.../miauto/bluetooth/MiBluetoothUtils.kt
a17 实现(标准 Android API):
// MiBluetoothUtils.kt:82 —— 返回标准 BluetoothAdapter
@JvmStatic
fun getAdapter(): BluetoothAdapter {
return BaseApplication.getGlobalApp()
.getSystemService(BluetoothManager::class.java)
.adapter
}dev 实现(settingslib 包装类,反射兼容 XCD/DCD):
// MiBluetoothUtils.kt:76 —— 返回 LocalBluetoothAdapter(settingslib 自有类)
@JvmStatic
fun getAdapter(): LocalBluetoothAdapter {
return getLocalBtManager().bluetoothAdapter
}两种 Adapter 的 API 差异对照(适配核心)
| 操作 | 标准 BluetoothAdapter (a17) | LocalBluetoothAdapter (dev) | 适配注意 |
|---|---|---|---|
| 设扫描模式 | setScanMode(mode) 方法 | scanMode = mode 属性 setter | 语法不同,直接 cp 会编译错 |
| 开始扫描 | startDiscovery() | startScanning(force: Boolean) | dev 的 force=true 绕过 settingslib 5min 节流 |
| 取扫描模式 | getScanMode() | getScanMode()(一致) | — |
| 取已配对集 | getBondedDevices(): Set<BluetoothDevice> | 无此便捷方法(dev 走 cachedDevice.bondState) | a17 能实时查系统,dev 只能查缓存 |
| 开关蓝牙 | enable() / getState() | enable() / getBluetoothState() | getState vs getBluetoothState 方法名不同 |
| 地址 | getAddress() | getAddress()(一致) | — |
连锁影响(字段类型也要改)
controller/BluetoothStateController.java:
// a17
private BluetoothAdapter mBluetoothAdapter;
// dev
private LocalBluetoothAdapter mBluetoothAdapter;view/BluetoothMainEntrySettingsFragment.java:enableBluetooth():
// a17 —— 用 MiBluetoothUtils.getAdapter() 拿标准 BluetoothAdapter
BluetoothAdapter bluetoothAdapter = MiBluetoothUtils.getAdapter();
if (bluetoothAdapter.getState() != BluetoothAdapter.STATE_OFF) { ... }
ThreadPoolUtils.ASYNC.execute(bluetoothAdapter::enable);
// dev —— 用 mLocalBluetoothManager 拿 LocalBluetoothAdapter
LocalBluetoothAdapter localBluetoothAdapter = mLocalBluetoothManager.getBluetoothAdapter();
if (localBluetoothAdapter.getBluetoothState() != BluetoothAdapter.STATE_OFF) { ... }
ThreadPoolUtils.ASYNC.execute(localBluetoothAdapter::enable);适配要点:跨版本搬运蓝牙代码时,第一步先确认目标分支的
MiBluetoothUtils.getAdapter()返回类型,再决定用哪套 API。如果目标平台标准BluetoothAdapter可用,优先 a17 写法;若必须兼容老平台(XCD/DCD 反射),保留 dev 的LocalBluetoothAdapter。
2. 配对状态判定:缓存 bondState vs 系统实时 getBondedDevices()
根因延伸:标准 BluetoothAdapter 有 getBondedDevices(),settingslib 的 LocalBluetoothAdapter 没有 → a17 能查系统真实配对态,dev 只能依赖 CachedBluetoothDevice.bondState(应用重启后可能过期,返回 BOND_NONE)。
共 3 处,写法对称:
2.1 MiBluetoothUtils.bondedDeviceFilter()
// a17 (MiBluetoothUtils.kt:166) —— 实时查系统
fun bondedDeviceFilter(cachedDevice: CachedBluetoothDevice): Boolean {
val bondedDevices = getAdapter().bondedDevices // 标准 API
val matches = bondedDevices != null && bondedDevices.contains(cachedDevice.device)
return matches
}
// dev (MiBluetoothUtils.kt:156) —— 查缓存
fun bondedDeviceFilter(cachedDevice: CachedBluetoothDevice): Boolean {
val matches = cachedDevice.bondState == BluetoothDevice.BOND_BONDED // 缓存值
return matches
}2.2 controller/BluetoothOperationsController.java:updateConnectionButtonState()
// a17 —— 实时查系统,解决"重启后缓存未同步"
boolean isBonded = MiBluetoothUtils.getAdapter()
.getBondedDevices()
.contains(getCachedDevice().getDevice());
if (!isBonded) { goBack(); }
// dev —— 查缓存
if (getCachedDevice().getBondState() == BluetoothDevice.BOND_NONE) { goBack(); }2.3 preference/NewBluetoothDevicePreference.java:refresh()
// a17 —— 实时查系统算 latestBondState
boolean isSystemBonded = MiBluetoothUtils.getAdapter()
.getBondedDevices().contains(mCachedDevice.getDevice());
int latestBondState = isSystemBonded ? BluetoothDevice.BOND_BONDED : BluetoothDevice.BOND_NONE;
// dev —— 直接取缓存
int latestBondState = mCachedDevice.getBondState();适配要点:若目标分支是
LocalBluetoothAdapter(dev 系),无法直接用getBondedDevices()——要么退回缓存 bondState,要么改成BluetoothManager.getAdapter().getBondedDevices()(绕过 settingslib 直接拿标准 adapter)。后者在老平台可用性需实测。
3. 扫描流程差异(startDiscovery / setScanMode / AlwaysDiscoverable)
文件:MiBluetoothUtils.kt(核心)+ BluetoothSwitchController.java
3.1 开始扫描 startDiscovery()
// a17 (MiBluetoothUtils.kt:211) —— 标准 API + 幂等守卫 + AlwaysDiscoverable
fun startDiscovery() {
if (isDiscovering()) { // ① 幂等:已在扫描则跳过
MLog.tag(TAG).d("startDiscovery: already discovering, skip")
return
}
getAdapter().startDiscovery() // ② 标准 API
mAlwaysDiscoverable.start() // ③ 保持可发现模式
}
// dev (MiBluetoothUtils.kt:196) —— settingslib API + 手动设 scanMode
fun startDiscovery() {
stopDiscovery()
setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
getAdapter().startScanning(true) // startScanning(force) 绕过 5min 节流
}3.2 设置扫描模式 setScanMode()
// a17 (MiBluetoothUtils.kt:190) —— 方法调用
fun setScanMode(mode: Int) {
getAdapter().setScanMode(mode) // BluetoothAdapter.setScanMode()
}
// dev (MiBluetoothUtils.kt:176) —— 属性赋值
fun setScanMode(mode: Int) {
getAdapter().scanMode = mode // LocalBluetoothAdapter.scanMode setter
}3.3 结束扫描 stopDiscovery()
// a17 (MiBluetoothUtils.kt:231) —— 停 AlwaysDiscoverable(会自动恢复 CONNECTABLE)
fun stopDiscovery() {
mAlwaysDiscoverable.stop()
if (isDiscovering()) { getAdapter().cancelDiscovery() }
}
// dev (MiBluetoothUtils.kt:210) —— 无 AlwaysDiscoverable,需调用方手动恢复 scanMode
fun stopDiscovery() {
if (isDiscovering()) { getAdapter().cancelDiscovery() }
}3.4 AlwaysDiscoverable 内部类(a17 独有,整段新增)
仅 a17 有(MiBluetoothUtils.kt:393-449),对齐 AOSP BluetoothScanningDevicesGroupPreferenceController.AlwaysDiscoverable:
// a17 独有 —— 监听 ACTION_SCAN_MODE_CHANGED,扫描期间被系统改模式时自动恢复 DISCOVERABLE
private class AlwaysDiscoverable : BroadcastReceiver() {
fun start() {
registerReceiver(this, IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED))
setDiscoverable() // 设为 SCAN_MODE_CONNECTABLE_DISCOVERABLE
}
fun stop() {
unregisterReceiver(this)
setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE) // 退出恢复可连接
}
override fun onReceive(...) { setDiscoverable() } // 被改了就改回来
}适配要点:
- a17 的”保持可发现”靠
AlwaysDiscoverable;dev 靠调用方(onPause/onDestroy)手动 setScanMode。两套机制不能混用——搬 AlwaysDiscoverable 必须连带改 stopDiscovery 的调用方(见第 4.4 节)。- dev 的
startScanning(true)绕过节流是 settingslib 特性,搬 a17 标准写法后节流逻辑需要自己实现(a17 靠isDiscovering()幂等 + AlwaysDiscoverable 替代)。
4. 扫描触发、列表重建、生命周期(BluetoothUnbondedDevicesPrefController)
文件:controller/BluetoothUnbondedDevicesPrefController.java(差异最密集,~98 行)
4.1 扫描触发幂等(BluetoothScanManager.kt:triggerScan)
// a17 —— 幂等检查
if (!MiBluetoothUtils.isDiscovering()) {
MiBluetoothUtils.startDiscovery()
} else {
MLog.tag(TAG).d("triggerScan: already discovering ..., skip")
}
// dev —— 无检查,直接触发
MiBluetoothUtils.startDiscovery()4.2 配对中抑制扫描(onScanningStateChanged)
// a17 —— 扫描结束时,先检查是否有设备正在配对(BOND_BONDING),有则不触发扫描
if (!started && mBondState != BluetoothDevice.BOND_BONDING) {
boolean hasBondingDevice = false;
for (Map.Entry<String, NewBluetoothDevicePreference> entry : mPreferenceMap.entrySet()) {
CachedBluetoothDevice device = entry.getValue().getCachedDevice();
if (device != null && device.getBondState() == BluetoothDevice.BOND_BONDING) {
hasBondingDevice = true;
break;
}
}
if (!hasBondingDevice) {
BluetoothScanManager.INSTANCE.triggerScan("onScanningStateChanged");
}
}
// dev —— 直接触发
if (!started && mBondState != BluetoothDevice.BOND_BONDING) {
BluetoothScanManager.INSTANCE.triggerScan("onScanningStateChanged");
}4.3 未配对列表缓存重建(innerUpdateState)
// a17 —— 对齐 AOSP updateState(),从 CachedDeviceManager 缓存重建列表
// (解决 DeviceFoundHandler 对已缓存设备不触发 onDeviceAdded → 列表空)
// 配对进行中(BOND_BONDING)跳过,避免误判刚配对设备
if (mBondState != BluetoothDevice.BOND_BONDING) {
for (CachedBluetoothDevice cachedDevice
: getBluetoothManager().getCachedDeviceManager().getCachedDevicesCopy()) {
if (MiBluetoothUtils.scannedDeviceFilter(cachedDevice)) {
addPreference(cachedDevice);
}
}
}
// 然后 triggerScan 补充新设备
ThreadUtils.postOnBackgroundThread(() -> BluetoothScanManager.INSTANCE.triggerScan("update_state"));
// dev —— 不重建,只 triggerScan
ThreadUtils.postOnBackgroundThread(() -> BluetoothScanManager.INSTANCE.triggerScan("update_state"));4.4 onScanStateChanged 的 SDK 版本分支
// a17 —— Android 15+(API 35+) 扫描统一交 onScanningStateChanged 接管
if (Build.VERSION.SDK_INT > 34) {
MLog.tag(TAG).d("onScanStateChanged: scan trigger handled by onScanningStateChanged only");
} else {
BluetoothScanManager.INSTANCE.triggerScan("scan_state_change");
}
// dev —— 用 pause 状态判断
if (!mIsPauseState.get()) {
BluetoothScanManager.INSTANCE.triggerScan("scan_state_change");
}4.5 onDestroy / onPause 恢复 scanMode(与第 3 节 AlwaysDiscoverable 联动)
// a17 onPause —— 注释说"stopDiscovery() 内部会调 AlwaysDiscoverable.stop() 恢复"
MiBluetoothUtils.stopDiscovery(); // 依赖 AlwaysDiscoverable
// dev onPause —— 手动设回 CONNECTABLE
MiBluetoothUtils.stopDiscovery();
MiBluetoothUtils.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
// a17 onDestroy —— 同上,依赖 AlwaysDiscoverable
MiBluetoothUtils.stopDiscovery();
// dev onDestroy —— 手动判断 scanMode 再恢复
LocalBluetoothAdapter adapter = MiBluetoothUtils.getAdapter();
if (adapter == null) { return; }
if (adapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE) {
MiBluetoothUtils.stopDiscovery();
MiBluetoothUtils.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
}适配要点:onPause/onDestroy 的 scanMode 恢复策略必须与 startDiscovery/stopDiscovery 的机制配套。用 a17 的 AlwaysDiscoverable 就用 a17 的简化 onDestroy;用 dev 的手动方式就保留 dev 的判断。两者不可交叉,否则要么可发现模式没恢复(耗电/被扫),要么重复设置。
5. 异常保护:DeadObjectException(BluetoothSwitchController)
文件:controller/BluetoothSwitchController.java
// a17 —— 字段用 MiBluetoothUtils.getAdapter()(统一入口)+ try-catch 防 BT 服务死亡崩溃
private BluetoothAdapter mBluetoothAdapter = MiBluetoothUtils.getAdapter();
...
try {
if (mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
}
} catch (Exception e) {
MLog.e(e, "handleStateChanged: bluetooth service died");
}
// dev —— 用已废弃的 getDefaultAdapter(),无异常保护
private BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); // ⚠️ deprecated
...
if (mBluetoothAdapter.getScanMode() != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
mBluetoothAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE);
} // 无 try-catch,BT 服务死亡会崩适配要点:
BluetoothAdapter.getDefaultAdapter()自 API 31 起已废弃,应改getSystemService(BluetoothManager).adapter(a17 写法)。- try-catch 是真实防崩保护,搬 dev→a17 时不要丢;搬 a17→dev 时建议保留。
6. 连接态 UI 链路(mIsConnectingState,a17 独有)
a17 有一条 dev 没有的「连接中」状态透传链,跨 3 个文件:
6.1 preference/NewBluetoothDevicePreference.java(字段 + 方法)
// a17 独有字段
protected Boolean mIsConnectingState = false;
// a17 —— setEnabled 双条件(busy 或 connecting 都禁用点击)
setEnabled(!mCachedDevice.isBusy() && !mIsConnectingState);
// a17 独有方法(供 Controller 调用)
public void setConnecting(Boolean isConnecting) {
mIsConnectingState = isConnecting;
}
// dev —— 单条件
setEnabled(!mCachedDevice.isBusy());
// 无 mIsConnectingState 字段、无 setConnecting 方法6.2 controller/BluetoothBondedDevicesPrefController.java(调用方)
// a17 —— 把 profile 连接状态透传给 UI
if (preference instanceof MiCarBluetoothBondedDevicePreference bondedPref) {
bondedPref.setConnecting(state == BluetoothProfile.STATE_CONNECTING);
}
// dev —— 无此分支
if (preference instanceof NewBluetoothDevicePreference) { ... } // 只 refreshDeviceUi6.3 preference/MiCarBluetoothBondedDevicePreference.kt(显示逻辑)
// a17 —— 双条件,更精准(busy 且确实是用户主动连接才显示"连接中")
} else if (isBusy && mIsConnectingState) {
context.getString(R.string.bluetooth_bonded_device_connecting)
}
// dev —— 单条件,后台 busy 也会误显示"连接中"
} else if (isBusy) {
context.getString(R.string.bluetooth_bonded_device_connecting)
}适配要点:这 3 个文件是一套,搬要一起搬(字段+方法+调用方+显示)。若只搬一个文件会编译错(
setConnecting找不到)或 UI 状态不一致。
7. 其他小差异(风格/重构/平台 workaround)
| 差异点 | 文件 | a17 | dev | 性质 |
|---|---|---|---|---|
| 配对按钮回调参数 | MiBluetoothPairingController.kt | CompoundButton(非空) + mPasskeyFormatted?.let{ setPin(it) } | CompoundButton?(可空) + setPin(mPasskeyFormatted) | 纯风格,等价 |
| 配对对话框基类 | MiBluetoothPairingDialog.kt | CarUIAlertDialogForKotlin | CarUIAlertDialog | dev 统一了基类 |
| 配对 Activity 设备缓存 | MiBluetoothPairingDialogActivity.kt | 否定按钮回调里临时 findDevice(device) | handlePairingIntent 入口一次 findDevice 存 mCachedPairingDevice,全程复用 + 双空判早退 | dev 小重构 |
| 设备详情页背景 | MiCarBluetoothDeviceDetailFragment.java | 纯色兜底(注释「A17 f3设备未加载出背景,临时增加颜色」) | ViewShadowUtils.setBlurAndBlend 正式磨砂 | a17 f3 平台临时 workaround |
| 开蓝牙入口 | BluetoothMainEntrySettingsFragment.java | MiBluetoothUtils.getAdapter() + getState() | mLocalBluetoothManager.getBluetoothAdapter() + getBluetoothState() | 见第 1 节根因 |
详情页背景这条要特别注意:a17 的纯色是 f3 平台临时方案(f3 上磨砂背景加载不出来)。若 f3 已修复背景问题,应同步 dev 的正式磨砂实现;若未修复,保留 a17 兜底。
8. settingslib 层差异(f3dif,平台适配)
适配同学如需动 settingslib 层(profile/事件总线),看这里。
8.1 flavor 绑定(base/settingsLibAndroid/build.gradle)
| 编译 flavor | 绑定源集 | 两分支 |
|---|---|---|
dcd | dcddif | 都有,字节一致 |
xcd_cn_a14 / xcd | xcddif | 都有,字节一致 |
xcd_cn_a17 / xcd_global_a17 | f3dif | 仅 a17 有 |
→ dev 编译走 dcddif/xcddif,a17 编译走 f3dif。dcddif/xcddif 两分支零差异,settingslib 层的差异全部集中在 f3dif。
8.2 f3dif 是上游 AOSP settingslib(比 xcddif 新)
- 顶层 bluetooth 类:f3dif 52 个 vs xcddif 45 个
- f3dif 多 9 个 Kotlin Ext 协程类(把回调式改 Flow 式):
BluetoothEventManagerExt、LocalBluetoothManagerExt、LeAudioProfileExt等 - f3dif 缺 4 个 xcddif 有的类:
DeviceGroupClientProfile、DunServerProfile、HearingAidStatsLogUtils、VcpProfile(移植 f3dif 能力时按需补齐)
8.3 f3dif 新增的蓝牙能力子目录(dcddif/xcddif 都没有)
| 子目录 | 能力 | 关键类/API |
|---|---|---|
devicesettings/ | 设备级设置(每设备自定义设置页) | 6 个 AIDL + Preference UI + Kotlin data 层 |
hearingdevices/ | 助听设备(HAP 预设/环境音量) | PresetController(BluetoothHapClient) + AmbientVolumeController(Android15 AudioInputControl) |
| LE Audio Sharing 增强 | BluetoothUtils.java 多 audioSharingHysteresisModeFix 等 | — |
适配要点:这些新能力当前只在 a17 的 f3dif flavor 编译时生效。dev/XCD 平台要用,需移植 settingslib 子目录 + 补 4 个缺失类 + 评估
AudioInputControl等 Android 15 API 的平台支持。
9. 适配清单与工作量评估
「方向」:回流 = a17→dev(修 bug,推荐);同步 = dev→a17(小重构)
| # | 差异点 | 影响文件 | 适配动作 | 平台相关? | 难度 |
|---|---|---|---|---|---|
| 1 | Adapter 类型 (BluetoothAdapter↔LocalBluetoothAdapter) | MiBluetoothUtils + 所有调用方 | 确认目标平台标准 API 可用性,统一一套 | 是 | ⭐⭐⭐ |
| 2 | 实时 getBondedDevices 替代缓存 | MiBluetoothUtils/Operations/NewPref | 改 3 处 bondState 判定 | 部分 | ⭐⭐ |
| 3 | startDiscovery + AlwaysDiscoverable | MiBluetoothUtils | 整段替换 + 内部类 | 是 | ⭐⭐ |
| 4 | setScanMode 语法(方法↔属性) | MiBluetoothUtils | 跟随 #1 | 是 | ⭐ |
| 5 | 扫描幂等 + 配对中抑制 | BluetoothScanManager + UnbondedController | 加 isDiscovering 守卫 + hasBondingDevice 检查 | 否 | ⭐⭐ |
| 6 | 未配对列表缓存重建 | UnbondedController.innerUpdateState | 加 for 循环重建 + BOND_BONDING 跳过 | 否 | ⭐⭐ |
| 7 | SDK>34 分支 | UnbondedController.onScanStateChanged | 加版本判断 | 是 | ⭐ |
| 8 | onDestroy scanMode 恢复策略 | UnbondedController | 跟随 #3 配套改 | 否 | ⭐ |
| 9 | DeadObjectException 保护 | BluetoothSwitchController | 加 try-catch + 去 getDefaultAdapter | 否 | ⭐ |
| 10 | 连接态 UI 链 (mIsConnectingState) | NewPref + BondedController + MiCarBondedPref | 3 文件一起搬 | 否 | ⭐⭐ |
| 11 | Dialog 基类/PairingActivity 缓存 | PairingDialog/Activity | 小重构 | 否 | ⭐ |
| 12 | 详情页磨砂背景 | DeviceDetailFragment | f3 修复后同步 | 是 | ⭐ |
| 13 | f3dif 新能力 (LE Audio/助听/设备设置) | settingslib/f3dif/* | 移植子目录 + 补缺失类 + build.gradle | 是 | ⭐⭐⭐⭐ |
建议优先级:
- 先做(低风险高收益,修真实 bug):#9 防崩、#2 配对态实时、#5/#6 扫描健壮性、#10 连接态 UI —— 这些不依赖平台,可直接回流。
- 谨慎做(平台相关):#1/#3/#4 Adapter 体系、#7 SDK 分支、#12 背景 —— 必须先在目标平台验证标准 API/新 API 可用。
- 按需做(新功能):#13 f3dif 上游能力 —— 单独立项。
附录:复验命令
# 查任意文件两版 diff(-行=a17,+行=dev)
git diff dev_a17_0610 dev -- \
settingsPage/micarConnectionSettings/src/main/java/com/android/car/settings/miauto/bluetooth/<文件>
# 看 a17 版完整文件
git show dev_a17_0610:<路径>
# 看 dev 版完整文件
git show dev:<路径>
# f3dif flavor 绑定
git show dev_a17_0610:base/settingsLibAndroid/build.gradle | sed -n '15,77p'