dev vs dev_a17_0610 蓝牙适配差异详解(底层代码级)

面向:蓝牙底层适配工程师 目的:逐处讲清两版蓝牙代码在 API / 字段 / 逻辑 上的具体差异,供评估跨版本/跨平台适配工作 产出:2026-07-24 | 根因 = Adapter 类型体系不同(第 1 节,必读) 配套:决策视角见 dev-vs-dev_a17_0610蓝牙差异报告


0. 怎么读这份文档

版本约定

  • a17 = dev_a17_0610 分支(Android 17 工具链,编译 flavor xcd_cn_a17/xcd_global_a17,settingslib 走 f3dif
  • dev = dev 分支(编译 flavor dcd/xcd,settingslib 走 dcddif/xcddif
  • 当前产线分支 rls-xcd-u-mp-26-v6.0dev 风格

代码块标注:每个差异点给出 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()

根因延伸:标准 BluetoothAdaptergetBondedDevices(),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) { ... }   // 只 refreshDeviceUi

6.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)

差异点文件a17dev性质
配对按钮回调参数MiBluetoothPairingController.ktCompoundButton(非空) + mPasskeyFormatted?.let{ setPin(it) }CompoundButton?(可空) + setPin(mPasskeyFormatted)纯风格,等价
配对对话框基类MiBluetoothPairingDialog.ktCarUIAlertDialogForKotlinCarUIAlertDialogdev 统一了基类
配对 Activity 设备缓存MiBluetoothPairingDialogActivity.kt否定按钮回调里临时 findDevice(device)handlePairingIntent 入口一次 findDevicemCachedPairingDevice,全程复用 + 双空判早退dev 小重构
设备详情页背景MiCarBluetoothDeviceDetailFragment.java纯色兜底(注释「A17 f3设备未加载出背景,临时增加颜色」)ViewShadowUtils.setBlurAndBlend 正式磨砂a17 f3 平台临时 workaround
开蓝牙入口BluetoothMainEntrySettingsFragment.javaMiBluetoothUtils.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绑定源集两分支
dcddcddif都有,字节一致
xcd_cn_a14 / xcdxcddif都有,字节一致
xcd_cn_a17 / xcd_global_a17f3dif仅 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 式):BluetoothEventManagerExtLocalBluetoothManagerExtLeAudioProfileExt
  • f3dif 缺 4 个 xcddif 有的类:DeviceGroupClientProfileDunServerProfileHearingAidStatsLogUtilsVcpProfile(移植 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.javaaudioSharingHysteresisModeFix

适配要点:这些新能力当前只在 a17 的 f3dif flavor 编译时生效。dev/XCD 平台要用,需移植 settingslib 子目录 + 补 4 个缺失类 + 评估 AudioInputControl 等 Android 15 API 的平台支持。


9. 适配清单与工作量评估

「方向」:回流 = a17→dev(修 bug,推荐);同步 = dev→a17(小重构)

#差异点影响文件适配动作平台相关?难度
1Adapter 类型 (BluetoothAdapter↔LocalBluetoothAdapter)MiBluetoothUtils + 所有调用方确认目标平台标准 API 可用性,统一一套⭐⭐⭐
2实时 getBondedDevices 替代缓存MiBluetoothUtils/Operations/NewPref改 3 处 bondState 判定部分⭐⭐
3startDiscovery + AlwaysDiscoverableMiBluetoothUtils整段替换 + 内部类⭐⭐
4setScanMode 语法(方法↔属性)MiBluetoothUtils跟随 #1
5扫描幂等 + 配对中抑制BluetoothScanManager + UnbondedController加 isDiscovering 守卫 + hasBondingDevice 检查⭐⭐
6未配对列表缓存重建UnbondedController.innerUpdateState加 for 循环重建 + BOND_BONDING 跳过⭐⭐
7SDK>34 分支UnbondedController.onScanStateChanged加版本判断
8onDestroy scanMode 恢复策略UnbondedController跟随 #3 配套改
9DeadObjectException 保护BluetoothSwitchController加 try-catch + 去 getDefaultAdapter
10连接态 UI 链 (mIsConnectingState)NewPref + BondedController + MiCarBondedPref3 文件一起搬⭐⭐
11Dialog 基类/PairingActivity 缓存PairingDialog/Activity小重构
12详情页磨砂背景DeviceDetailFragmentf3 修复后同步
13f3dif 新能力 (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'