杰理 SDK 文档中心
首页
首页
  • 项目概览

    • 仓库简介与示例构成
    • 支持的平台与协议
  • 快速开始

    • 导入工程与编译运行
    • 蓝牙权限配置
  • 应用架构

    • 工程结构与模块分层
    • 核心类与回调接口
  • 核心功能

    • 蓝牙设备扫描
    • ATT 设备连接与断开管理
    • 数据收发与通知回调
  • 配置与调试

    • 协议配置常量
    • 日志系统与调试指南
  • 界面与交互

    • 设备扫描与连接界面
    • 设备详情与设置界面

设备扫描与连接界面

设备管理页面(DeviceFragment)是 ATTConnect 演示应用中负责发现、展示并连接经典蓝牙(BR/EDR)设备的入口界面,它通过 BtScanner 驱动系统蓝牙发现机制,借助 DeviceViewModel 桥接 BleManager 完成设备连接,并以两个列表(可连接设备列表、已连接设备列表)呈现扫描与连接状态。

Purpose and Scope

本文档介绍「设备扫描与连接界面」这条完整能力链路:从权限申请、蓝牙发现、扫描结果去重/更新,到点击连接、连接状态观察与失败回退。覆盖以下实现文件:

  • DeviceFragment.kt:界面控制器,权限回调、列表渲染、错误重试入口
  • DeviceViewModel.kt:扫描/连接的业务逻辑与状态暴露
  • BtScanner.kt:基于系统 BluetoothAdapter.startDiscovery() 的扫描器单例
  • ScanDeviceAdapter.kt:扫描结果列表适配器(去重与 RSSI 增量更新)

BLE 底层连接细节(BleManager、BleEventCallbackManager)与设备详情页(DeviceDetailsFragment)属于兄弟能力,不在本页展开,相关内容见「Related Links」。

Overview

设备扫描与连接界面是用户在应用启动后首先接触的核心页面(HomeActivity 的 Tab 之一,标题取 R.string.tab_device)。它的职责链条为:

  1. 权限前置:Android 12+(API 31)需要 BLUETOOTH_SCAN / BLUETOOTH_CONNECT,低版本需要定位权限 ACCESS_COARSE_LOCATION / ACCESS_FINE_LOCATION,扫描前还要确认蓝牙与位置服务开关。
  2. 设备发现:DeviceViewModel.startScan() 调用 BtScanner.startScan(30 * 1000L, ...),通过系统经典蓝牙发现(startDiscovery)回调上报设备,扫描 30 秒后由主线程 Handler 触发超时停止。
  3. 结果呈现:ScanDeviceAdapter 将回调上来的 ScanDeviceInfo 去重合并(同名设备只保留一个条目,增量更新 RSSI/连接状态),已连接设备移入 ConnectedDeviceAdapter 列表。
  4. 连接发起:点击列表中处于 STATE_DISCONNECTED 的条目时,先停扫,再调用 BleManager.connectBleDevice(device, TRANSPORT_BREDR) 以 BR/EDR 传输发起连接。
  5. 状态观察:DeviceViewModel 通过 BluetoothViewModel.deviceStateMLD 观察全局连接状态变化,连接失败(RES_FAILURE)或断开都会以 OpResult 形式回传 UI。

设计上,界面层(Fragment)只负责视图与用户交互,业务逻辑全部下沉到 DeviceViewModel,扫描能力封装为 BtScanner 单例,连接能力复用全局 BleManager——这种分层让扫描器可以被其他页面复用,也让连接状态的单一数据源(deviceStateMLD)贯穿整个应用。

Architecture

flowchart TD
    subgraph sg_UI["UI 层 (ui/device)"]
        DF["DeviceFragment"]
        SDA["ScanDeviceAdapter"]
        CDA["ConnectedDeviceAdapter"]
        TD["TipsDialog"]
    end

    subgraph sg_VM["逻辑层 (ViewModel)"]
        DVM["DeviceViewModel"]
        BVM["BluetoothViewModel"]
    end

    subgraph sg_SCAN["扫描工具层 (tool/scan)"]
        BTS["BtScanner (单例)"]
        BR["BluetoothScanReceiver"]
        UH["uiHandler (主线程)"]
    end

    subgraph sg_BLE["BLE 连接层 (tool/ble)"]
        BM["BleManager"]
        ECM["BleEventCallbackManager"]
        MSG["deviceStateMLD / DeviceConnection"]
    end

    DF -->|"viewModels() 绑定"| DVM
    DF -->|"setAdapter"| SDA
    DF -->|"setAdapter"| CDA
    DF -->|"TipsDialog.Builder"| TD
    DVM -->|"继承"| BVM
    DVM -->|"startScan / stopScan"| BTS
    DVM -->|"connectBleDevice / disconnectBleDevice"| BM
    BTS -->|"注册/注销"| BR
    BTS -->|"MSG_SCAN_DEVICE_TIMEOUT"| UH
    BTS -->|"onDiscoveryDevice / onDiscoveryFail"| DVM
    BM -->|"状态回调"| ECM
    ECM -->|"DeviceConnection"| BVM
    SDA -->|"连接中状态"| DF

各组件职责说明:

  • DeviceFragment:生命周期管理者。负责 onResume 触发扫描、onPause 停止扫描、权限回调分发(permissions-dispatcher 注解生成)、错误页 viewNoneDevice 的重试按钮分发(按 tag 区分错误码跳转系统设置页)。
  • DeviceViewModel:状态中枢。暴露 scanStateMLD(扫描中)、foundDeviceMLD(发现设备)、opResultMLD(操作结果)三个 LiveData;内部维护 connectingDevice 记录"正在发起连接的设备",配合全局连接状态观察器判断连接成败。
  • BtScanner:单例扫描器。封装系统蓝牙发现 API,提供超时自动停止、广播接收、无名称设备过滤(Config.IS_FILTER_NO_NAME_DEVICE)、扫描开始时回放系统已连接设备列表等能力。
  • BleManager / BleEventCallbackManager:全局连接管理器(Java 实现),DeviceViewModel 只调用其 connectBleDevice / disconnectBleDevice,连接状态经由 deviceStateMLD(DeviceConnection)观察回来。

分层的关键意图:扫描与连接被刻意解耦——扫描器不关心连接,ViewModel 不关心广播细节;BtScanner 的回调接口 IBtScanCallback 是两者之间的唯一契约,使得将来替换为 BLE 扫描(LeScanCallback)或扩展扫描策略时,UI 层无需改动。

权限前置与扫描触发流程

扫描开始前必须通过两道关卡:系统权限 + 系统开关。DeviceFragment 使用 permissions.dispatcher 注解(@RuntimePermissions)生成权限处理代码,按 Android 版本拆分为两套权限:

平台版本所需权限失败处理
Android 12+ (API 31+)BLUETOOTH_SCAN、BLUETOOTH_CONNECTonBtPermissionDenied → handleStopScan(ERR_LACK_PERMISSION),错误页点击跳转应用详情设置
Android 11-ACCESS_COARSE_LOCATION、ACCESS_FINE_LOCATIONonLocationPermissionDenied → checkBtPermission(false) 继续走蓝牙权限检查

onViewCreated 中先注册 permissionHandler:当权限请求结果为 RESULT_OK 时立即 tryToScanDevice();onResume 同样调用 tryToScanDevice(false),保证从设置页返回后自动恢复扫描。

@RequiresApi(Build.VERSION_CODES.S)
@NeedsPermission(value = [Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT])
fun onBtPermissionGrant() {
    dismissPermissionTipsDialog()
    scanDevice()
}

@RequiresApi(Build.VERSION_CODES.S)
@OnPermissionDenied(value = [Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT])
fun onBtPermissionDenied() {
    dismissPermissionTipsDialog()
    handleStopScan(ERR_LACK_PERMISSION)
}

Source: DeviceFragment.kt

权限通过后进入 scanDevice() 流程,随后是系统开关检查(蓝牙未打开 → goToBluetoothSettings();位置服务未打开 → goToGpsServiceSettings()),并把错误码作为 tag 挂到空态页 viewNoneDevice.btnRetry 上,点击重试按钮时按错误码分派到对应系统设置页或直接重新扫描:

binding.viewNoneDevice.btnRetry.setOnClickListener {
    if (it.tag is Int) {
        when (it.tag) {
            ERR_LACK_PERMISSION -> { goToAppDetailsSettings() }
            ERR_BT_CLOSE -> { goToBluetoothSettings() }
            ERR_GPS_SERVICE_CLOSE -> { goToGpsServiceSettings() }
            else -> tryToScanDevice()
        }
    }
}

Source: DeviceFragment.kt

错误码常量定义在 DeviceFragment.Companion 中(DeviceFragment.kt#L49-L76):

常量值含义
ERR_SCAN_TIMEOUT0x1010搜索设备超时
ERR_LACK_PERMISSION0x1011缺少权限
ERR_SCAN_FAILED0x1012搜索设备失败
ERR_BT_CLOSE0x1013蓝牙未打开
ERR_GPS_SERVICE_CLOSE0x1014位置服务未打开

扫描实现机制(BtScanner)

BtScanner 是私有构造 + 双重检查锁的单例,持有 MyApplication.application 作为 Context,内部通过 BluetoothManager 获取 BluetoothAdapter:

fun getInstance(): BtScanner {
    if (null == instance) {
        synchronized(BtScanner::class.java) {
            if (null == instance) {
                instance = BtScanner(MyApplication.application)
            }
        }
    }
    return instance!!
}

Source: BtScanner.kt

startScan(timeout, callback) 的核心时序:

  1. 若 isScanning 为 true,直接回调 onDiscoveryFail(0x0100, "It is scanning."),防止重复扫描;
  2. 置 isScanning = true、暂存回调、注册 BluetoothScanReceiver;
  3. 在主线程 uiHandler 上投递 MSG_SCAN_DEVICE_TIMEOUT(延时 timeout 毫秒),超时即 stopScan();
  4. postDiscoveryState(true) 先上报扫描开始,并回放系统已连接设备列表(跳过 DEVICE_TYPE_LE 的纯 LE 设备,以 setRssi(0).setEnableConnect(true) 构造条目);
  5. 调用 btAdapter.startDiscovery(),若启动失败则回调 onDiscoveryFail(0x0101, "Failed to scan device.") 并停止扫描。
fun startScan(timeout: Long, callback: IBtScanCallback) {
    if (isScanning) {
        callback.onDiscoveryFail(0x0100, "It is scanning.")
        return
    }
    isScanning = true
    this.callback = callback
    registerReceiver()
    uiHandler.removeMessages(MSG_SCAN_DEVICE_TIMEOUT)
    if (timeout > 0) {
        uiHandler.sendEmptyMessageDelayed(MSG_SCAN_DEVICE_TIMEOUT, timeout)
    }
    postDiscoveryState(true)
    if (!btAdapter.startDiscovery()) {
        callback.onDiscoveryFail(0x0101, "Failed to scan device.")
        stopScan()
    }
}

Source: BtScanner.kt

stopScan() 是幂等的:isScanning 为 false 直接返回;否则清理超时消息、cancelDiscovery()、注销广播接收器,最后 postDiscoveryState(false)——注意此时 callback 被置空,避免停止扫描后仍收到残留广播回调造成泄漏。

设备过滤逻辑集中在 postFoundDevice():当 Config.IS_FILTER_NO_NAME_DEVICE 开启时,BluetoothUtil.getDeviceName(device, false) 返回 "N/A" 的设备被丢弃,保证列表里只出现有名字的设备:

private fun postFoundDevice(scanInfo: ScanDeviceInfo) {
    if (Config.IS_FILTER_NO_NAME_DEVICE) {
        val name = BluetoothUtil.getDeviceName(scanInfo.device, false)
        if ("N/A" == name) return
    }
    callback?.onDiscoveryDevice(scanInfo)
}

Source: BtScanner.kt

BtScanner 同时暴露 destroy() 用于停止扫描并清空单例(便于测试重置)。广播接收器 BluetoothScanReceiver 负责监听 ACTION_FOUND / ACTION_DISCOVERY_STARTED / ACTION_DISCOVERY_FINISHED / ACTION_STATE_CHANGED,其中适配器状态变化通过 postBluetoothAdapterChange 上报,若正在扫描则立即停扫——这是对"蓝牙被用户关闭"这类运行时中断的标准防御。

ViewModel 的扫描编排

DeviceViewModel 将扫描参数固定为 30 秒超时,并通过 IBtScanCallback 把扫描事件映射为 UI 可观察的 LiveData:

fun startScan() {
    if (isScanning()) return
    btScanner.startScan(30 * 1000L, object : IBtScanCallback {
        override fun onDiscoveryState(bStart: Boolean) {
            scanStateMLD.value = bStart
        }
        override fun onDiscoveryDevice(scanDeviceInfo: ScanDeviceInfo?) {
            scanDeviceInfo?.let {
                it.connection = getDeviceConnection(it.device)
                foundDeviceMLD.value = it
            }
        }
        override fun onDiscoveryFail(code: Int, message: String?) {
            opResultMLD.postValue(OpResult(OP_SCAN_DEVICE, code, message ?: ""))
        }
    })
}

Source: DeviceViewModel.kt

关键设计点:

  • 连接状态回填:每次上报设备时调用 getDeviceConnection(it.device)(来自 BluetoothViewModel,底层读取 BleManager 的连接列表),把该设备当前的连接状态写入 ScanDeviceInfo.connection,这样列表项能即时显示"连接中/已连接"而不必等下一次状态事件。
  • 线程安全:扫描回调来自系统广播(主线程),而连接失败等场景用 postValue 兼容子线程触发;foundDeviceMLD 使用 setValue(主线程回调)保证顺序。
  • 操作码约定:OP_SCAN_DEVICE = 0x01、OP_CONNECT_DEVICE = 0x02 作为 OpResult 的操作标识,UI 侧据其区分扫描失败与连接失败。

连接流程与状态观察

点击发起连接

列表点击事件在 DeviceFragment.initUI() 中注册,仅当条目处于 STATE_DISCONNECTED 时才允许点击(防止对连接中/已连接设备重复发起);点击后先停扫再连接:

scanDeviceAdapter.setOnItemClickListener { _, _, position ->
    val scanInfo = scanDeviceAdapter.getItem(position)
    if (scanInfo.connection != BluetoothProfile.STATE_DISCONNECTED) return@setOnItemClickListener
    viewModel.stopScan()
    viewModel.connectATTDevice(scanInfo)
}

Source: DeviceFragment.kt

DeviceViewModel.connectATTDevice() 记录目标设备后调用 BleManager.connectBleDevice(device, BleManager.TRANSPORT_BREDR)——显式指定 BR/EDR 传输(经典蓝牙),与 BtScanner 的 startDiscovery 发现机制对应,不依赖 BLE 连接:

fun connectATTDevice(scanInfo: ScanDeviceInfo): Boolean {
    connectingDevice = scanInfo.device
    val ret = bleManager.connectBleDevice(scanInfo.device, BleManager.TRANSPORT_BREDR)
    if (!ret) {
        connectingDevice = null
        opResultMLD.postValue(
            OpResult(
                OP_CONNECT_DEVICE,
                OpResult.RES_FAILURE,
                "Failed to connect device.",
                scanInfo.device.address
            )
        )
    }
    return ret
}

Source: DeviceViewModel.kt

连接结果判定

BleManager 的连接状态统一通过 BluetoothViewModel.deviceStateMLD(类型 DeviceConnection)广播。DeviceViewModel 在 init 中注册 connectionObserver 并 observeForever(不依赖 Fragment 生命周期,保证扫描/连接跨越页面切换时仍能收到结果),onCleared 中移除:

private val connectionObserver = Observer<DeviceConnection> {
    if (BluetoothUtil.deviceEquals(connectingDevice, it.device)) {
        if (it.state != BluetoothProfile.STATE_CONNECTING) {
            connectingDevice = null
            if (it.state == BluetoothProfile.STATE_DISCONNECTING || it.state == BluetoothProfile.STATE_DISCONNECTED) {
                opResultMLD.postValue(
                    OpResult(
                        OP_CONNECT_DEVICE,
                        OpResult.RES_FAILURE,
                        "Failed to connect device.",
                        it.device.address
                    )
                )
            }
        }
    }
}

Source: DeviceViewModel.kt

判定逻辑的意图:

  • 只有"我主动发起连接的那个设备"(connectingDevice 比对)的状态变化才被处理,避免被其他设备的状态事件干扰;
  • STATE_CONNECTING 是进行中态,不结束等待;
  • 进入 DISCONNECTING/DISCONNECTED 说明连接失败或中途断开,以 RES_FAILURE 上报;连接成功(STATE_CONNECTED)则靠 foundDeviceMLD/适配器状态更新反映,无需失败提示。

列表状态联动

DeviceFragment 观察 deviceStateMLD 后同时驱动两个适配器:可连接列表调用 ScanDeviceAdapter.updateItem(connection),已连接列表由 ConnectedDeviceAdapter 维护。ScanDeviceAdapter.updateItem 在设备连接成功后将其从可连接列表移除,保证"可连接"与"已连接"两个列表互斥:

fun updateItem(connection: DeviceConnection) {
    val scanDevice = findScanDevice(connection.device) ?: return
    scanDevice.connection = connection.state
    if (scanDevice.connection == BluetoothProfile.STATE_CONNECTED) {
        remove(scanDevice)
    } else {
        notifyItemChanged(getItemPosition(scanDevice))
    }
}

Source: ScanDeviceAdapter.kt

已连接列表的条目点击会弹出 TipsDialog(TipsDialog.Builder().content(...))确认后执行断开操作(DeviceViewModel.disconnectDevice → BleManager.disconnectBleDevice)。

扫描结果列表的增量更新

ScanDeviceAdapter 继承 BaseQuickAdapter(BRVAH 框架),重写 addData 实现按设备地址去重的增量合并:新设备直接追加;已存在的设备只更新 rssi、rawData、connection、isEnableConnect 四个字段并局部刷新,避免 notifyDataSetChanged 全量闪烁(被注释掉的排序方案即为此取舍):

@SuppressLint("NotifyDataSetChanged")
override fun addData(data: ScanDeviceInfo) {
    if (data.connection == BluetoothProfile.STATE_CONNECTED) return
    val index = this.data.indexOf(data)
    if (index == -1) {
        super.addData(data)
    } else {
        val item = getItem(index)
        item.rssi = data.rssi
        item.rawData = data.rawData
        item.connection = data.connection
        item.isEnableConnect = data.isEnableConnect
        notifyItemChanged(index)
    }
}

Source: ScanDeviceAdapter.kt

convert 中每行展示:设备名(BluetoothUtil.getDeviceName,不读缓存)、地址 + RSSI 副标题("%s\tRSSI: %d"),以及一个 AVLoadingIndicatorView 加载动画——仅当 connection == STATE_CONNECTING 时可见,直观表达"正在连接":

holder.getView<AVLoadingIndicatorView>(R.id.aiv_loading).also {
    if (item.connection == BluetoothProfile.STATE_CONNECTING) {
        it.visibility = View.VISIBLE
        it.show()
    } else {
        it.hide()
        it.gone()
    }
}

Source: ScanDeviceAdapter.kt

Core Flow

sequenceDiagram
    participant U as 用户
    participant F as DeviceFragment
    participant VM as DeviceViewModel
    participant S as BtScanner
    participant B as BleManager
    participant OS as Android 系统

    U->>F: 进入设备页 (onResume)
    F->>F: tryToScanDevice(false)
    F->>OS: 请求定位/蓝牙权限
    OS-->>F: 授权结果
    F->>F: 检查蓝牙/位置服务开关
    F->>VM: startScan()
    VM->>S: startScan(30s, IBtScanCallback)
    S->>OS: startDiscovery()
    OS-->>S: ACTION_FOUND (广播)
    S->>VM: onDiscoveryDevice(ScanDeviceInfo)
    VM->>VM: connection = getDeviceConnection(device)
    VM-->>F: foundDeviceMLD 更新
    F->>F: ScanDeviceAdapter.addData 去重/更新
    U->>F: 点击设备条目 (STATE_DISCONNECTED)
    F->>VM: stopScan() + connectATTDevice(info)
    VM->>S: stopScan()
    VM->>B: connectBleDevice(device, TRANSPORT_BREDR)
    B-->>VM: deviceStateMLD: STATE_CONNECTING
    VM-->>F: 列表项显示加载动画
    B-->>VM: deviceStateMLD: STATE_CONNECTED
    VM->>VM: connectingDevice = null
    F->>F: ScanDeviceAdapter.updateItem → remove
    Note over F: 设备移入已连接列表
    B-->>VM: deviceStateMLD: DISCONNECTING/DISCONNECTED
    VM-->>F: opResultMLD: OP_CONNECT_DEVICE FAILURE

生命周期与并发控制

  • 页面可见性:onResume 触发扫描、onPause 若在扫描则 stopScan(),配合 BtScanner 的 isScanning 状态位,避免页面不可见时继续消耗蓝牙资源。
  • 下拉刷新:SwipeRefreshLayout(srlDevices)的刷新回调先 delay(800) 再做停扫+重启扫描,且用 Dispatchers.Main 复位刷新动画,防止快速下拉导致扫描器状态错乱。
  • 重复扫描防护:startScan 入口双重判断(ViewModel 的 isScanning() 与 BtScanner 内部的 isScanning 短路),即使 UI 连点也不会并发启动两次 startDiscovery。
  • 回调生命周期管理:BtScanner.postDiscoveryState(false) 时清空 callback;BtScanner 提供 destroy() 重置单例,测试或退出时可彻底释放广播接收器。

Usage Examples

示例 1:以 30 秒超时启动扫描并消费结果

btScanner.startScan(30 * 1000L, object : IBtScanCallback {
    override fun onAdapterChange(bEnabled: Boolean) {}
    override fun onDiscoveryState(bStart: Boolean) {
        scanStateMLD.value = bStart
    }
    override fun onDiscoveryDevice(scanDeviceInfo: ScanDeviceInfo?) {
        scanDeviceInfo?.let {
            it.connection = getDeviceConnection(it.device)
            foundDeviceMLD.value = it
        }
    }
    override fun onDiscoveryFail(code: Int, message: String?) {
        opResultMLD.postValue(OpResult(OP_SCAN_DEVICE, code, message ?: ""))
    }
})

Source: DeviceViewModel.kt

示例 2:以 BR/EDR 传输发起连接(经典蓝牙)

fun connectATTDevice(scanInfo: ScanDeviceInfo): Boolean {
    connectingDevice = scanInfo.device
    val ret = bleManager.connectBleDevice(scanInfo.device, BleManager.TRANSPORT_BREDR)
    if (!ret) {
        connectingDevice = null
        opResultMLD.postValue(
            OpResult(
                OP_CONNECT_DEVICE,
                OpResult.RES_FAILURE,
                "Failed to connect device.",
                scanInfo.device.address
            )
        )
    }
    return ret
}

Source: DeviceViewModel.kt

示例 3:驱动系统经典蓝牙发现并处理启动失败

if (!btAdapter.startDiscovery()) {
    callback.onDiscoveryFail(0x0101, "Failed to scan device.")
    stopScan()
}

Source: BtScanner.kt

Configuration Options

配置项类型默认值说明
Config.IS_FILTER_NO_NAME_DEVICEBoolean由 Config.kt 定义扫描时过滤无名称设备(getDeviceName 返回 "N/A" 的丢弃),影响 BtScanner.postFoundDevice()
扫描超时Long30 * 1000L硬编码于 DeviceViewModel.startScan(),传给 BtScanner.startScan(timeout, ...),超时由主线程 Handler 触发停扫
传输类型IntBleManager.TRANSPORT_BREDR连接时显式指定经典蓝牙传输,确保与 startDiscovery 发现的设备匹配

注:Config.IS_FILTER_NO_NAME_DEVICE 在 BtScanner.kt#L152 被消费,具体取值以 Config.kt 为准。

API Reference

DeviceViewModel(设备界面逻辑)

方法签名说明
startScanfun startScan()以 30s 超时启动扫描;已在扫描中则直接返回。回调经 IBtScanCallback 映射为 scanStateMLD / foundDeviceMLD / opResultMLD
stopScanfun stopScan()委托 btScanner.stopScan(),幂等
connectATTDevicefun connectATTDevice(scanInfo: ScanDeviceInfo): Boolean记录 connectingDevice 并调用 BleManager.connectBleDevice(device, TRANSPORT_BREDR);返回连接是否成功发起,失败时向 opResultMLD 投递 OP_CONNECT_DEVICE / RES_FAILURE
disconnectDevicefun disconnectDevice(device: BluetoothDevice?)委托 BleManager.disconnectBleDevice 断开指定设备
getConnectedDevicesfun getConnectedDevices(): List<BluetoothDevice>读取 bleManager.connectedDeviceList 当前已连接设备
isScanningfun isScanning(): Boolean读取 btScanner.isScanning 状态位

公开状态(LiveData):

  • scanStateMLD: MutableLiveData<Boolean> — 扫描开始/结束
  • foundDeviceMLD: MutableLiveData<ScanDeviceInfo> — 发现/更新设备
  • opResultMLD: MutableLiveData<OpResult<Any>> — 扫描与连接操作结果(含错误码与消息)

操作码: OP_SCAN_DEVICE = 0x01、OP_CONNECT_DEVICE = 0x02

Source: DeviceViewModel.kt

BtScanner(蓝牙扫描器单例)

方法签名说明
getInstancefun getInstance(): BtScanner双重检查锁单例,Context 取 MyApplication.application
startScanfun startScan(timeout: Long, callback: IBtScanCallback)启动系统发现;已扫描中回调 onDiscoveryFail(0x0100);timeout > 0 时投递主线程超时消息;startDiscovery() 失败回调 onDiscoveryFail(0x0101) 并停扫
stopScanfun stopScan()取消发现、注销广播、清空回调并上报 onDiscoveryState(false)
destroyfun destroy()停扫并置空单例实例
isScanningvar isScanning: Boolean (private set)扫描状态位,仅内部修改

Source: BtScanner.kt

ScanDeviceAdapter(扫描结果适配器)

方法签名说明
addDataoverride fun addData(data: ScanDeviceInfo)重写去重合并:地址已存在则更新 rssi/rawData/connection/isEnableConnect 并 notifyItemChanged;已连接设备直接忽略
updateItemfun updateItem(connection: DeviceConnection)按连接状态刷新条目;STATE_CONNECTED 时从列表移除
findScanDeviceprivate fun findScanDevice(device: BluetoothDevice): ScanDeviceInfo?通过 BluetoothUtil.deviceEquals 按地址查找

Source: ScanDeviceAdapter.kt

Failure Modes、边界情况与并发

场景触发条件处理方式
重复扫描已 isScanning 时再次调用 startScanBtScanner 内部短路,回调 onDiscoveryFail(0x0100, "It is scanning.")
startDiscovery 失败系统拒绝启动发现(如并发被占用)回调 onDiscoveryFail(0x0101) 并自动 stopScan() 恢复状态
扫描超时30s 未结束主线程 MSG_SCAN_DEVICE_TIMEOUT 触发 stopScan()
权限缺失用户拒绝扫描/连接权限ERR_LACK_PERMISSION 空态页,重试跳应用设置
蓝牙未开启系统适配器关闭ERR_BT_CLOSE 空态页,重试跳蓝牙设置
位置服务关闭Android 11- 需要位置服务ERR_GPS_SERVICE_CLOSE 空态页,重试跳位置设置
连接发起失败connectBleDevice 返回 false立即复位 connectingDevice,opResultMLD 投递 OP_CONNECT_DEVICE/RES_FAILURE
连接断开/失败连接中进入 DISCONNECTING/DISCONNECTEDconnectionObserver 复位 connectingDevice 并上报失败
蓝牙运行时关闭扫描中用户关闭蓝牙postBluetoothAdapterChange 回调 + 自动停扫
快速下拉刷新高频触发 SwipeRefresh800ms 延时合并 + 停扫重扫,避免扫描器状态竞争

并发要点: 所有广播回调运行在主线程,BtScanner 的状态位读写均在同一线程,无需加锁;跨线程场景(如连接结果 postValue)使用 MutableLiveData.postValue 保证线程安全。connectingDevice 是 ViewModel 内单线程访问的"进行中连接"标记,杜绝多设备同时连接时的状态串扰。

Performance / Operational 说明

  • RSSI 增量更新:ScanDeviceAdapter.addData 只 notifyItemChanged(index),避免高频发现广播引发全列表重绘(源码中保留的排序方案被注释即为此权衡)。
  • 资源释放:onPause 停扫 + postDiscoveryState(false) 清空回调引用,防止页面退栈后广播泄漏;BtScanner.destroy() 可整体重置单例。
  • 系统已连接设备回放:扫描开始时同步注入系统级已连接设备列表(跳过 LE 设备),让用户无需等待发现广播即可看到已连设备,提升首屏体验。
  • 广播接收器注册:registerReceiver/unregisterReceiver 与扫描生命周期严格配对,避免 IllegalArgumentException(重复注册/未注销)。

Extension Points

  1. 替换扫描策略:IBtScanCallback 是扫描器与上层唯一的契约。如需支持 BLE 扫描(BluetoothLeScanner.startScan),只需新增一个实现 IBtScanCallback 的扫描器并在 DeviceViewModel 中替换 btScanner,UI 层零改动。
  2. 过滤规则扩展:BtScanner.postFoundDevice() 中的 Config.IS_FILTER_NO_NAME_DEVICE 可扩展为按厂商/服务 UUID 过滤。
  3. 连接类型扩展:DeviceViewModel.connectATTDevice 显式使用 TRANSPORT_BREDR,可仿照 BleManager 的其他传输常量扩展双模连接。
  4. 错误页定制:viewNoneDevice.btnRetry 的错误码 tag 分派机制可无缝增加新的错误类型与对应设置入口。

Related Links

  • 设备详情页(DeviceDetailsFragment / DeviceDetailsViewModel):连接成功后展示设备信息的兄弟页面
  • 蓝牙连接管理(BleManager.java):connectBleDevice / disconnectBleDevice 的底层实现
  • 蓝牙事件回调(BleEventCallbackManager.java / DeviceConnection):deviceStateMLD 状态事件的来源
  • 扫描设备模型(ScanDeviceInfo.kt):列表条目数据结构
  • 已连接设备适配器(ConnectedDeviceAdapter.kt):已连接列表的渲染
  • 通用蓝牙 ViewModel(BluetoothViewModel.kt):deviceStateMLD 与 getDeviceConnection 的基类实现
  • 工具类 BluetoothUtil:设备名、地址、deviceEquals、系统连接列表等工具方法
Next
设备详情与设置界面