设备扫描与连接界面
设备管理页面(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)。它的职责链条为:
- 权限前置:Android 12+(API 31)需要
BLUETOOTH_SCAN/BLUETOOTH_CONNECT,低版本需要定位权限ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION,扫描前还要确认蓝牙与位置服务开关。 - 设备发现:
DeviceViewModel.startScan()调用BtScanner.startScan(30 * 1000L, ...),通过系统经典蓝牙发现(startDiscovery)回调上报设备,扫描 30 秒后由主线程 Handler 触发超时停止。 - 结果呈现:
ScanDeviceAdapter将回调上来的ScanDeviceInfo去重合并(同名设备只保留一个条目,增量更新 RSSI/连接状态),已连接设备移入ConnectedDeviceAdapter列表。 - 连接发起:点击列表中处于
STATE_DISCONNECTED的条目时,先停扫,再调用BleManager.connectBleDevice(device, TRANSPORT_BREDR)以 BR/EDR 传输发起连接。 - 状态观察:
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_CONNECT | onBtPermissionDenied → handleStopScan(ERR_LACK_PERMISSION),错误页点击跳转应用详情设置 |
| Android 11- | ACCESS_COARSE_LOCATION、ACCESS_FINE_LOCATION | onLocationPermissionDenied → 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_TIMEOUT | 0x1010 | 搜索设备超时 |
ERR_LACK_PERMISSION | 0x1011 | 缺少权限 |
ERR_SCAN_FAILED | 0x1012 | 搜索设备失败 |
ERR_BT_CLOSE | 0x1013 | 蓝牙未打开 |
ERR_GPS_SERVICE_CLOSE | 0x1014 | 位置服务未打开 |
扫描实现机制(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) 的核心时序:
- 若
isScanning为 true,直接回调onDiscoveryFail(0x0100, "It is scanning."),防止重复扫描; - 置
isScanning = true、暂存回调、注册BluetoothScanReceiver; - 在主线程
uiHandler上投递MSG_SCAN_DEVICE_TIMEOUT(延时timeout毫秒),超时即stopScan(); postDiscoveryState(true)先上报扫描开始,并回放系统已连接设备列表(跳过DEVICE_TYPE_LE的纯 LE 设备,以setRssi(0).setEnableConnect(true)构造条目);- 调用
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_DEVICE | Boolean | 由 Config.kt 定义 | 扫描时过滤无名称设备(getDeviceName 返回 "N/A" 的丢弃),影响 BtScanner.postFoundDevice() |
| 扫描超时 | Long | 30 * 1000L | 硬编码于 DeviceViewModel.startScan(),传给 BtScanner.startScan(timeout, ...),超时由主线程 Handler 触发停扫 |
| 传输类型 | Int | BleManager.TRANSPORT_BREDR | 连接时显式指定经典蓝牙传输,确保与 startDiscovery 发现的设备匹配 |
注:
Config.IS_FILTER_NO_NAME_DEVICE在 BtScanner.kt#L152 被消费,具体取值以 Config.kt 为准。
API Reference
DeviceViewModel(设备界面逻辑)
| 方法 | 签名 | 说明 |
|---|---|---|
startScan | fun startScan() | 以 30s 超时启动扫描;已在扫描中则直接返回。回调经 IBtScanCallback 映射为 scanStateMLD / foundDeviceMLD / opResultMLD |
stopScan | fun stopScan() | 委托 btScanner.stopScan(),幂等 |
connectATTDevice | fun connectATTDevice(scanInfo: ScanDeviceInfo): Boolean | 记录 connectingDevice 并调用 BleManager.connectBleDevice(device, TRANSPORT_BREDR);返回连接是否成功发起,失败时向 opResultMLD 投递 OP_CONNECT_DEVICE / RES_FAILURE |
disconnectDevice | fun disconnectDevice(device: BluetoothDevice?) | 委托 BleManager.disconnectBleDevice 断开指定设备 |
getConnectedDevices | fun getConnectedDevices(): List<BluetoothDevice> | 读取 bleManager.connectedDeviceList 当前已连接设备 |
isScanning | fun 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(蓝牙扫描器单例)
| 方法 | 签名 | 说明 |
|---|---|---|
getInstance | fun getInstance(): BtScanner | 双重检查锁单例,Context 取 MyApplication.application |
startScan | fun startScan(timeout: Long, callback: IBtScanCallback) | 启动系统发现;已扫描中回调 onDiscoveryFail(0x0100);timeout > 0 时投递主线程超时消息;startDiscovery() 失败回调 onDiscoveryFail(0x0101) 并停扫 |
stopScan | fun stopScan() | 取消发现、注销广播、清空回调并上报 onDiscoveryState(false) |
destroy | fun destroy() | 停扫并置空单例实例 |
isScanning | var isScanning: Boolean (private set) | 扫描状态位,仅内部修改 |
Source: BtScanner.kt
ScanDeviceAdapter(扫描结果适配器)
| 方法 | 签名 | 说明 |
|---|---|---|
addData | override fun addData(data: ScanDeviceInfo) | 重写去重合并:地址已存在则更新 rssi/rawData/connection/isEnableConnect 并 notifyItemChanged;已连接设备直接忽略 |
updateItem | fun updateItem(connection: DeviceConnection) | 按连接状态刷新条目;STATE_CONNECTED 时从列表移除 |
findScanDevice | private fun findScanDevice(device: BluetoothDevice): ScanDeviceInfo? | 通过 BluetoothUtil.deviceEquals 按地址查找 |
Source: ScanDeviceAdapter.kt
Failure Modes、边界情况与并发
| 场景 | 触发条件 | 处理方式 |
|---|---|---|
| 重复扫描 | 已 isScanning 时再次调用 startScan | BtScanner 内部短路,回调 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/DISCONNECTED | connectionObserver 复位 connectingDevice 并上报失败 |
| 蓝牙运行时关闭 | 扫描中用户关闭蓝牙 | postBluetoothAdapterChange 回调 + 自动停扫 |
| 快速下拉刷新 | 高频触发 SwipeRefresh | 800ms 延时合并 + 停扫重扫,避免扫描器状态竞争 |
并发要点: 所有广播回调运行在主线程,BtScanner 的状态位读写均在同一线程,无需加锁;跨线程场景(如连接结果 postValue)使用 MutableLiveData.postValue 保证线程安全。connectingDevice 是 ViewModel 内单线程访问的"进行中连接"标记,杜绝多设备同时连接时的状态串扰。
Performance / Operational 说明
- RSSI 增量更新:
ScanDeviceAdapter.addData只notifyItemChanged(index),避免高频发现广播引发全列表重绘(源码中保留的排序方案被注释即为此权衡)。 - 资源释放:
onPause停扫 +postDiscoveryState(false)清空回调引用,防止页面退栈后广播泄漏;BtScanner.destroy()可整体重置单例。 - 系统已连接设备回放:扫描开始时同步注入系统级已连接设备列表(跳过 LE 设备),让用户无需等待发现广播即可看到已连设备,提升首屏体验。
- 广播接收器注册:
registerReceiver/unregisterReceiver与扫描生命周期严格配对,避免IllegalArgumentException(重复注册/未注销)。
Extension Points
- 替换扫描策略:
IBtScanCallback是扫描器与上层唯一的契约。如需支持 BLE 扫描(BluetoothLeScanner.startScan),只需新增一个实现IBtScanCallback的扫描器并在DeviceViewModel中替换btScanner,UI 层零改动。 - 过滤规则扩展:
BtScanner.postFoundDevice()中的Config.IS_FILTER_NO_NAME_DEVICE可扩展为按厂商/服务 UUID 过滤。 - 连接类型扩展:
DeviceViewModel.connectATTDevice显式使用TRANSPORT_BREDR,可仿照BleManager的其他传输常量扩展双模连接。 - 错误页定制:
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、系统连接列表等工具方法