Jetpack Bluetooth——更优雅地使用蓝牙

2 阅读15分钟

Jetpack Bluetooth——更优雅地使用蓝牙

image.png

蓝牙是安卓开发中非常常用的操作,但安卓经过这么多年的迭代,蓝牙的相关接口都经过了很多修改需要适配,还有的接口需要实现一堆函数。。。整套操作虽说不算复杂,但难免感觉不太舒服。

之前在看 Jetpack 库更新页面时突然眼前一亮,发现了一个名叫 Bluetooth 的新库,就立马进去看了看。

image.png

接下来看下官方是这么描述的:

This is the initial release of AndroidX Bluetooth APIs that provides a Kotlin API surface covering Bluetooth LE scanning and advertising, and GATT client and server use cases. It provides a minimal API surface, clear thread model with async and sync operations, and ensures all methods be executed and provides the results.

官方终于看不下去了,决定自己搞一把,虽然只是 alpha 版本,但也可以去看看源码嘛!

image.png

很显然,全是 Kotlin 代码,虽然 Google 大力推崇 Kotlin 已经很久了,但其实还有很大一部分人对 Kotlin 嗤之以鼻,这也不怪别人,毕竟移动端已经凉了,安卓也凉了,还在坚持安卓的人本就不多,只是想吃饭而已。

推出这个库的用意很明确,简化大家使用蓝牙的步骤,且都是 Kotlin 代码,里面的 API 也都使用了 Flow ,大大简化了蓝牙的操作复杂度。

写过蓝牙相关的朋友对 BluetoothLeScannerBluetoothManagerBluetoothAdapter 等类肯定非常熟悉,也打过了很多交道,但如我上面所说,使用起来还是不太方便的,但如果使用了 Bluetooth 这个新库,都使用不到这些系统类,或者说使用到了,但是是没有感知的。

扫描蓝牙设备

之前实现步骤

先来看下没有这个库之前咱们想实现的话需要如何做吧:

1、在 AndroidManifest 中添加蓝牙相关的权限,动态申请权限这块就省略不写了

<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

2、在需要使用蓝牙功能的类中,获取系统蓝牙适配器实例

private lateinit var bluetoothManager: BluetoothManager
private lateinit var bluetoothAdapter: BluetoothAdapter
​
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
​
    // 获取蓝牙管理器
    bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    // 获取蓝牙适配器
    bluetoothAdapter = bluetoothManager.adapter
    
    // 检查蓝牙是否可用
    if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) {
        // 提示用户开启蓝牙或处理无法使用蓝牙的情况
    }
}

3、创建一个实现了 ScanCallback 接口的类,用于接收扫描结果:

private val scanCallback = object : ScanCallback() {
    override fun onScanResult(callbackType: Int, result: ScanResult) {
        super.onScanResult(callbackType, result)
​
        val device = result.device
        val deviceName = device.name ?: "Unknown"
        val deviceAddress = device.address
        val rssi = result.rssi
​
        Log.d(TAG, "发现BLE设备: 名称 - $deviceName, 地址 - $deviceAddress, 信号强度 - $rssi")
​
        // 在此处处理扫描到的设备信息,如添加到列表、更新UI等
    }
​
    override fun onBatchScanResults(results: MutableList<ScanResult>) {
        super.onBatchScanResults(results)
        // 如果需要处理批量扫描结果,可以在此处进行
    }
​
    override fun onScanFailed(errorCode: Int) {
        super.onScanFailed(errorCode)
        Log.w(TAG, "BLE扫描失败,错误代码: $errorCode")
        // 根据错误码处理扫描失败情况
    }
}

4、使用 BluetoothLeScanner 启动扫描,指定扫描参数(如扫描过滤条件、扫描模式、扫描周期等)以及上面创建的回调

fun startBleScan() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        val settings = ScanSettings.Builder()
            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 或选择其他适合的扫描模式
            .build()
​
        val filters = mutableListOf<ScanFilter>() // 可以添加过滤条件,如特定的服务UUID等
​
        bluetoothAdapter.bluetoothLeScanner.startScan(filters, settings, scanCallback)
    } else {
        // 对于低于Android 5.0的设备,可以考虑使用startLeScan函数,但请注意其已废弃并可能有性能和稳定性问题
        bluetoothAdapter.startLeScan { device, rssi, _ ->
            val deviceName = device.name ?: "Unknown"
            val deviceAddress = device.address
            Log.d(TAG, "遗留BLE扫描: 名称 - $deviceName, 地址 - $deviceAddress, 信号强度 - $rssi")
            // 处理扫描到的设备信息
        }
    }
}

5、在不需要继续扫描时,调用相应函数停止扫描

fun stopBleScan() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
        bluetoothAdapter.bluetoothLeScanner.stopScan(scanCallback)
    } else {
        bluetoothAdapter.stopLeScan(scanCallback as BluetoothAdapter.LeScanCallback) // 注意类型转换
    }
}

Bluetooth 实现步骤

下面来看下使用了 Bluetooth 后的实现方式吧:

1、第一步肯定也是添加权限,上面已经写过,这块不再赘述

2、第二步进行初始化

val bluetoothLe = BluetoothLe(context)

3、第三步进行扫描

val scanResultFlow = bluetoothLe.scan(
        listOf(
            ScanFilter(
                serviceDataUuid = UUIDS.UUID_FOR_FILTER,
                deviceName = "XXX"
            )
        )
    )

4、对扫描出的结果进行观察

scanResultFlow.collect {
        Log.i(TAG, "Greeting: 888888:${it.name}")
    }

区别不能说不大,API 也不能说不简单,之前需要的代码挺多,而现在加起来不到十行代码;而且之前都需要自己在回调中处理消息,而现在直接给封装为了 Flow ,更加适合现在的编程习惯。

还有很重要的一点,之前需要自己来控制扫描的开始和结束,而现在开发者无需关注这些,只需要关注数据得到之后的处理就可以了。

GATT 连接

GATT-Generic Attribute profle-通用属性配置文件。GATT层是传输真正数据所在的层。包括了一个数据传输和存储架构以及其基本操作。GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理。

之前实现步骤

1、获取扫描结果中的 BluetoothDevice 对象

val bluetoothDevice = it.device

2、创建BluetoothGatt客户端并发起连接

val gattCallback = MyGattCallback() // 实现自定义的BluetoothGattCallback
val bluetoothGatt = bluetoothDevice.connectGatt(context, false, gattCallback)

3、实现BluetoothGattCallback接口

class MyGattCallback : BluetoothGattCallback() {
    override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
        when (newState) {
            BluetoothProfile.STATE_CONNECTED -> {
                Log.i(TAG, "已连接到GATT服务器。")
                gatt.discoverServices() // 连接成功后触发服务发现
            }
            BluetoothProfile.STATE_DISCONNECTED -> {
                Log.i(TAG, "已断开与GATT服务器的连接。")
                // 在此处处理断开连接的逻辑
            }
        }
    }
​
    override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            // 在此处发现并处理服务及特征
            // 使用gatt.services访问可用服务
        } else {
            Log.w(TAG, "onServicesDiscovered 接收到状态码: $status")
        }
    }
​
    // 根据需要覆盖其他相关函数,如onCharacteristicRead、onCharacteristicWrite等
}

4、管理BluetoothGatt连接生命周期:若要断开与设备的连接,先调用 bluetoothGatt.disconnect() ,然后调用bluetoothGatt.close() 释放资源。

Bluetooth 实现步骤

看到上面一坨挺难受,来看看 Bluetooth 会不会让整个步骤优雅一点!

val bluetoothDevice = it.device
bluetoothLe.connectGatt(device = it.device) {
    val gattService = getService("GATT_BATTERY_SERVICE_UUID")
    val batteryLevel = gattService?.getCharacteristic("GATT_BATTERY_LEVEL_UUID") ?: return@connectGatt
    val result = readCharacteristic(batteryLevel)
    if (result.isSuccess) {
        val value = result.getOrNull() ?: return@connectGatt
        val battery = value[0].toInt() and 0xff
        Log.i(TAG, "battery:$battery")
    }
}

代码更少,实现的内容反而更多,只能说两个字:优雅!

这才是代码,当然这也依赖于 Kotlin 的一些特性,但确实看着舒服了许多。更重要的是,之前调用的时候需要考虑生命周期来进行 disconnectclose 来释放资源,现在完全不需要了,Bluetooth 库中都帮我们做好了。

探索魔法

Bluetooth 的能力刚才在上面简单展示了下,但它是如何实现的呢?咱们点进源码来看看!

刚才不管是扫描还是连接 GATT 都使用到了 BluetoothLe ,这也是这个库的入口:

class BluetoothLe(context: Context) {
​
    private val bluetoothManager =
        context.getSystemService(Context.BLUETOOTH_SERVICE) as FwkBluetoothManager?
    private val bluetoothAdapter = bluetoothManager?.adapter
​
    var advertiseImpl: AdvertiseImpl? =
        bluetoothAdapter?.bluetoothLeAdvertiser?.let(::getAdvertiseImpl)
​
    var scanImpl: ScanImpl? =
        bluetoothAdapter?.bluetoothLeScanner?.let(::getScanImpl)
​
    val client: GattClient by lazy(LazyThreadSafetyMode.PUBLICATION) {
        GattClient(context.applicationContext)
    }
​
    val server: GattServer by lazy(LazyThreadSafetyMode.PUBLICATION) {
        GattServer(context.applicationContext)
    }
​
    // 返回一个冷流来启动蓝牙LE Advertise
    fun advertise(advertiseParams: AdvertiseParams): Flow<@AdvertiseResult Int> {
        return advertiseImpl?.advertise(advertiseParams) ?: callbackFlow {
            close(AdvertiseException(AdvertiseException.UNSUPPORTED))
        }
    }
​
    // 返回一个冷流启动蓝牙LE扫描。扫描是用来发现附近的Advertise设备。
    fun scan(filters: List<ScanFilter> = emptyList()): Flow<ScanResult> {
        return scanImpl?.scan(filters) ?: callbackFlow {
            close(ScanException(ScanException.UNSUPPORTED))
        }
    }
​
    // 连接到远程蓝牙设备上的GATT服务器,并在建立连接后调用高阶函数
    suspend fun <R> connectGatt(
        device: BluetoothDevice,
        block: suspend GattClientScope.() -> R
    ): R {
        return client.connect(device, block)
    }
​
    // 打开GATT服务器
    fun openGattServer(services: List<GattService>): GattServerConnectFlow {
        return server.open(services)
    }
}

代码其实并不多,当然这块把一些注释和一些无用的代码给隐藏了,即使都放进来也没有多少,一共几十行代码,咱们从上到下看一遍都可以!

一共有六个全局变量,还有四个函数,先来看下全局变量吧:

  • bluetoothManager :这个没啥说的,直接使用传入的 context 来构建出来,虽说咱们在调用的时候感知不到 bluetoothManager 的存在,但肯定也得有啊,库再厉害,封装的接口再好也得回归到调用系统接口不是!这块还有个比较好玩的东西,大家可以看到它的类型不是 BluetoothManager 而是 FwkBluetoothManager ,这又是 Kotlin 特性的一个使用了,在看这块的时候我还纳闷了下,一看上面 import 则会心一笑。
  • bluetoothAdapter :这也没啥说的,调用系统蓝牙必须的接口
  • advertiseImplAdvertise 的实现类,调用了一个叫 getAdvertiseImpl 的函数生成的,一会咱们可以看下
  • scanImpl :扫描的实现类,同 Advertise 一样,也是调用了一个函数生成的
  • client :顾名思义,是 GATT 的客户端
  • server :同样,是 GATT 的服务端

Advertise

变量说完来看下函数吧!按照顺序先来看 advertise 吧!这个函数中就用到了上面的 advertiseImpl ,它的类型是 AdvertiseImpl ,先来看看 AdvertiseImpl 是个啥吧!

interface AdvertiseImpl {
    fun advertise(advertiseParams: AdvertiseParams): Flow<@BluetoothLe.AdvertiseResult Int>
}

这就清晰多了,AdvertiseImpl 原来不是个实现类。。。而是一个接口😂。这不重要,有可能这就是他们的规范吧!

里面只有一个函数 advertise ,返回了一个 Flow ,函数只有一个形参:AdvertiseParams ,虽然还没看 AdvertiseParams 的代码,但根据名字能知道肯定是构建 advertise 所需要的参数,来看看是不是!

class AdvertiseParams(
    // 设备地址是否包含在发布报文中
    @get:JvmName("shouldIncludeDeviceAddress")
    val shouldIncludeDeviceAddress: Boolean = false,
    // 是否将设备名称包含在发布报文中
    @get:JvmName("shouldIncludeDeviceName")
    val shouldIncludeDeviceName: Boolean = false,
    // Advertise是否会被发现
    val isConnectable: Boolean = false,
    // 以毫秒为单位的Advertise持续时间
    val isDiscoverable: Boolean = false,
    // 公司标识符到制造商特定数据的映射
    @IntRange(from = 0, to = 180000) val durationMillis: Long = 0,
    val manufacturerData: Map<Int, ByteArray> = emptyMap(),
    // 服务的16位uuid到相应的附加服务数据的映射
    val serviceData: Map<UUID, ByteArray> = emptyMap(),
    // 要发布的服务uuid列表
    val serviceUuids: List<UUID> = emptyList(),
    // 连接的服务请求uuid列表
    val serviceSolicitationUuids: List<UUID> = emptyList()
)

没错,就是 Advertise 所需的一些参数配置,当然类中还有一些别的内容,都是为了适配各个版本蓝牙的一些差异,这里不再赘述,想要看详细代码的话大家可以直接去看下。

咱们接着来看刚才构建出 AdvertiseImpl 的函数 getAdvertiseImpl

internal fun getAdvertiseImpl(bleAdvertiser: FwkBluetoothLeAdvertiser): AdvertiseImpl {
    return if (Build.VERSION.SDK_INT >= 26) AdvertiseImplApi26(bleAdvertiser)
    else AdvertiseImplBase(bleAdvertiser)
}

这样来看就清晰多了,在 BluetoothLe 中传入了 BluetoothLeAdvertiser 来调用 getAdvertiseImpl ,这块的 FwkBluetoothLeAdvertiser 同样也是使用了 Kotlin 中的 as 关键字,然后判断当前的 SDK 版本来决定使用哪种实现,没办法,这也是为了适配不同版本的蓝牙修改内容嘛,Google 种下的坑自己也得来填一下嘛!这块就来看一下 AdvertiseImplBase 吧!

private open class AdvertiseImplBase(val bleAdvertiser: FwkBluetoothLeAdvertiser) : AdvertiseImpl {
​
    @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE")
    override fun advertise(advertiseParams: AdvertiseParams) = callbackFlow {
        val callback = object : FwkAdvertiseCallback() {
            override fun onStartSuccess(settingsInEffect: FwkAdvertiseSettings) {
                trySend(BluetoothLe.ADVERTISE_STARTED)
            }
​
            override fun onStartFailure(errorCode: Int) {
                close(AdvertiseException(errorCode))
            }
        }
​
        bleAdvertiser.startAdvertising(
            advertiseParams.fwkAdvertiseSettings, advertiseParams.fwkAdvertiseData, callback
        )
        ...
        awaitClose {
            bleAdvertiser.stopAdvertising(callback)
        }
    }
}

有没有很清晰!直接通过 callbackFlow 来构建 Flow 作为返回值,然后实现下 AdvertiseCallback ,成功的话进行发送消息,失败的话进行关闭,之后传入相关参数真正执行 startAdvertising ,最后调用 awaitClose 来进行关闭动作,这也是为什么不用我们自己调用关闭的原因!

Scan

同样的,scanImpl 的类型是 ScanImpl ,来看下吧:

interface ScanImpl {
    val fwkSettings: FwkScanSettings
    fun scan(filters: List<ScanFilter> = emptyList()): Flow<ScanResult>
}

这个接口名称不再吐槽,直接看内容,一个变量一个函数,变量就是咱们熟悉的 ScanSettings ,函数就是执行扫描操作了,这里又有一个 ScanFilter ,这也能看出来就是扫描过滤的一个类:

class ScanFilter(
    // 远端设备地址的扫描过滤器
    val deviceAddress: BluetoothAddress? = null,
​
    // 远端设备名称的扫描过滤器
    val deviceName: String? = null,
​
    // 制造商id的扫描过滤器
    val manufacturerId: Int = MANUFACTURER_FILTER_NONE,
​
    // 制造商数据的扫描过滤器
    val manufacturerData: ByteArray? = null,
​
    // 对manufacturerData的部分过滤器
    val manufacturerDataMask: ByteArray? = null,
​
    // 服务数据uid的扫描过滤器
    val serviceDataUuid: UUID? = null,
​
    // 业务数据扫描过滤器
    val serviceData: ByteArray? = null,
​
    // 对业务数据的部分过滤
    val serviceDataMask: ByteArray? = null,
​
    // 服务uid的扫描过滤器
    val serviceUuid: UUID? = null,
​
    // 服务流体上的部分过滤器
    val serviceUuidMask: UUID? = null,
​
    // 服务请求uid的扫描过滤器
    val serviceSolicitationUuid: UUID? = null,
​
    // 服务请求uid上的部分过滤器
    val serviceSolicitationUuidMask: UUID? = null
)

这个类里面也有很多不同安卓版本的适配内容,这块也不再赘述,咱们只要知道这个类是用来配置过滤参数的就可以了!

接着来看 getScanImpl ,也就是用来构建 ScanImpl 的函数:

internal fun getScanImpl(bluetoothLeScanner: FwkBluetoothLeScanner): ScanImpl {
    return if (Build.VERSION.SDK_INT >= 26) ScanImplApi26(bluetoothLeScanner)
    else ScanImplBase(bluetoothLeScanner)
}

同样,不同安卓版本需要不同的实现。。。。同样直接看下 Base 实现:

private open class ScanImplBase(val bluetoothLeScanner: FwkBluetoothLeScanner) : ScanImpl {
​
    override val fwkSettings: FwkScanSettings = FwkScanSettings.Builder()
        .build()
​
    @RequiresPermission("android.permission.BLUETOOTH_SCAN")
    override fun scan(filters: List<ScanFilter>): Flow<ScanResult> = callbackFlow {
        val callback = object : FwkScanCallback() {
            override fun onScanResult(callbackType: Int, result: FwkScanResult) {
                trySend(ScanResult(result))
            }
​
            override fun onScanFailed(errorCode: Int) {
                close(ScanException(errorCode))
            }
        }
​
        val fwkFilters = filters.map { it.fwkScanFilter }
​
        bluetoothLeScanner.startScan(fwkFilters, fwkSettings, callback)
​
        awaitClose {
            bluetoothLeScanner.stopScan(callback)
        }
    }
}

实现了接口 ScanImpl ,然后配置了下 ScanSettings ,之后就是执行真正的扫描了,和 Advertise类似,也是使用了 callbackFlowawaitClose 的配套操作,省去了咱们的关闭操作!剩下的还是调用系统接口,将异步转为 Flow

connectGatt

连接 GATT 这一步操作原来需要如何做上面也贴了代码,其实那只是非常简单的使用,如果要实现更多功能的话则需要更多的代码,当使用了 Bluetooth 之后变得如此简单都要归功于 GattClient

来直接看下 connect 这个函数吧:

suspend fun <R> connect(
    device: BluetoothDevice,
    block: suspend GattClientScope.() -> R
): R = coroutineScope {
    val connectResult = CompletableDeferred<Unit>(parent = coroutineContext.job)
    val callbackResultsFlow =
        MutableSharedFlow<CallbackResult>(extraBufferCapacity = Int.MAX_VALUE)
    val subscribeMap = mutableMapOf<FwkBluetoothGattCharacteristic, SubscribeListener>()
    val subscribeMutex = Mutex()
    val attributeMap = AttributeMap()
    val servicesFlow = MutableStateFlow<List<GattService>>(listOf())
​
    val fwkCallback = object : FwkBluetoothGattCallback() {
        ....系统接口调用
    }
​
    if (!fwkAdapter.connectGatt(context, device.fwkDevice, fwkCallback)) {
        throw CancellationException("failed to connect")
    }
​
    withTimeout(CONNECT_TIMEOUT_MS) {
        connectResult.await()
    }
​
    val gattClientScope = object : GattClientScope {
        ....GattClientScope的实现
    }
​
    coroutineContext.job.invokeOnCompletion {
        fwkAdapter.closeGatt()
    }
​
    gattClientScope.block()
}

这样看着内容不多,实际上系统接口调用这块隐藏了三百多行代码,里面调用了很多系统接口,不能全弄出来了,太多了!

调用 coroutineScope 在特定作用域来开启一个协程来执行对应的系统接口,最后执行有 GattClientScope 作用域的高阶函数!那接下来就来看下 GattClientScope 吧!

interface GattClientScope {
​
    // 从远程设备发现的GATT服务流
    val servicesFlow: StateFlow<List<GattService>>
​
    // 最近从远程设备上发现的GATT服务
    val services: List<GattService> get() = servicesFlow.value
​
    // 按UUID获取远程设备的服务
    fun getService(uuid: UUID): GattService?
​
    // 从服务器读取特征值
    suspend fun readCharacteristic(characteristic: GattCharacteristic): Result<ByteArray>
​
    // 将特征值写入服务器
    suspend fun writeCharacteristic(
        characteristic: GattCharacteristic,
        value: ByteArray
    ): Result<Unit>
​
    // 返回包含给定特性的指示值的冷流
    fun subscribeToCharacteristic(characteristic: GattCharacteristic): Flow<ByteArray>
}

什么意思呢,就是当调用 connectGatt 函数后,里面的高阶函数就可以使用 GattClientScope 中的变量和函数,这里变量和函数的赋值就在上面代码中 GattClientScope的实现 那块。

综上所述,connectGatt 操作由 Bluetooth 帮我们做了很多事,甚至不用考虑生命周期及版本适配!

openGattServer

还有最后一个函数:openGattServer ,它使用的是 GattServer ,其实和 GattClient 类似,也是一大堆系统实现及版本适配,咱们直接来看函数实现吧:

fun open(services: List<GattService>): GattServerConnectFlow {
    return GattServerFlowImpl(services)
}

函数很短,传入对应参数,调用了 GattServerFlowImpl 后返回一个 GattServerConnectFlow ,返回的这是个什么东西,看名字像是 Flow ,来看下吧!

interface GattServerConnectFlow : Flow<GattServerConnectRequest> {
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun onOpened(action: suspend () -> Unit): GattServerConnectFlow
​
    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    fun onClosed(action: suspend () -> Unit): GattServerConnectFlow
    // 更新打开的GATT服务器提供的服务
    suspend fun updateServices(services: List<GattService>)
}

没错,确实还是个 Flow ,只不过扩展了下 Flow 的功能,增加了三个函数!具体实现 GattServerFlowImpl 这块不做描述。大家如果感兴趣的可以去看下源码,操作基本都是调用系统接口,然后做一些适配,再使用 Adapter 做一些兼容,最后通过 callbackFlowawaitClose 来转为 Flow 并确保关闭对应内容!

骚操作

这个库中其实有一些骚操作的,比如上面说的使用了 as 关键字,咱们来看下:

import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
import android.bluetooth.BluetoothGatt as FwkBluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic as FwkBluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor as FwkBluetoothGattDescriptor
import android.bluetooth.BluetoothGattServer as FwkBluetoothGattServer
import android.bluetooth.BluetoothGattServerCallback as FwkBluetoothGattServerCallback
import android.bluetooth.BluetoothGattService as FwkBluetoothGattService
import android.bluetooth.BluetoothManager as FwkBluetoothManager

这只是冰山一角,整个库中有很多这样的用法,学习到了一波,但这个只是学习,日常开发中一般不会用到。

那么这个库中为什么要这么搞呢?首先是方便管理,因为本来就是一个依赖库,并且调用到了很多系统接口,且库中还有很多和系统接口同名的类。

说起这个咱们来看一个例子,比如上面扫描接口中返回的 Flow<ScanResult> ,此 ScanResult 并非咱们熟知的 ScanResult ,而是一个套壳!

class ScanResult @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
    private val fwkScanResult: FwkScanResult
) {
​
    companion object {
        // 报文中不存在周期性的Advertise间隔
        const val PERIODIC_INTERVAL_NOT_PRESENT: Int = FwkScanResult.PERIODIC_INTERVAL_NOT_PRESENT
    }
​
    @RequiresApi(29)
    private object ScanResultApi29Impl {
        @JvmStatic
        @DoNotInline
        fun serviceSolicitationUuids(fwkScanResult: FwkScanResult): List<ParcelUuid> =
            fwkScanResult.scanRecord?.serviceSolicitationUuids.orEmpty()
    }
​
    @RequiresApi(26)
    private object ScanResultApi26Impl {
        @JvmStatic
        @DoNotInline
        fun isConnectable(fwkScanResult: FwkScanResult): Boolean =
            fwkScanResult.isConnectable
​
        @JvmStatic
        @DoNotInline
        fun periodicAdvertisingInterval(fwkScanResult: FwkScanResult): Long =
            (fwkScanResult.periodicAdvertisingInterval * 1.25).toLong()
    }
​
    // 找到远程蓝牙设备
    val device: BluetoothDevice = BluetoothDevice(fwkScanResult.device)
​
    // 找到远程设备的蓝牙地址。
    val deviceAddress: BluetoothAddress = BluetoothAddress(
        fwkScanResult.device.address,
        BluetoothAddress.ADDRESS_TYPE_UNKNOWN
    )
​
    // 最后一次看到Advertise的设备时间戳。
    val timestampNanos: Long
        get() = fwkScanResult.timestampNanos
​
    // 返回与制造商id关联的特定于制造商的数据
    fun getManufacturerSpecificData(manufacturerId: Int): ByteArray? {
        return fwkScanResult.scanRecord?.getManufacturerSpecificData(manufacturerId)
    }
​
    // Advertise中用于标识蓝牙GATT服务的服务uuid列表
    val serviceUuids: List<UUID>
        get() = fwkScanResult.scanRecord?.serviceUuids?.map { it.uuid }.orEmpty()
​
    // 返回Advertise中用于标识蓝牙GATT服务的服务请求uuid列表。
    val serviceSolicitationUuids: List<ParcelUuid>
        get() = if (Build.VERSION.SDK_INT >= 29) {
            ScanResultApi29Impl.serviceSolicitationUuids(fwkScanResult)
        } else {
            emptyList()
        }
​
    // 返回服务UUID的映射及其相应的服务数据
    val serviceData: Map<ParcelUuid, ByteArray>
        get() = fwkScanResult.scanRecord?.serviceData.orEmpty()
​
    // 返回与服务UUID关联的服务数据
    fun getServiceData(serviceUuid: UUID): ByteArray? {
        return fwkScanResult.scanRecord?.getServiceData(ParcelUuid(serviceUuid))
    }
​
    // 检查该对象是否表示可连接的扫描结果
    fun isConnectable(): Boolean {
        return if (Build.VERSION.SDK_INT >= 26) {
            ScanResultApi26Impl.isConnectable(fwkScanResult)
        } else {
            true
        }
    }
​
    // 返回以dBm为单位的接收信号强度。取值范围为- 127,126
    val rssi: Int
        get() = fwkScanResult.rssi
​
    // 返回周期广告间隔,单位为毫秒,取值范围是7.5ms ~ 81918.75ms
    val periodicAdvertisingInterval: Long
        get() = if (Build.VERSION.SDK_INT >= 26) {
            // Framework returns interval in units of 1.25ms.
            ScanResultApi26Impl.periodicAdvertisingInterval(fwkScanResult)
        } else {
            PERIODIC_INTERVAL_NOT_PRESENT.toLong()
        }
}

我刚开始使用这个库的时候也是懵的,调用系统接口发现没有,点进去一看才恍然大悟,不止是 ScanResult 这一个类,BluetoothDeviceBluetoothDevice 等等也都是这样的套壳操作。

为什么要套壳呢?很简单,之前的不好用嘛!套壳之后保留了之前有用的接口,然后再增加一些需要的接口,两全其美!

还有就是刚才说到的 GattServerConnectFlow ,之前使用 Flow 都是直接使用,从来没有继承后写个子类,里面再写一些需要的函数,嗯,有这种观念需求的话这种方式挺好的。

总结

本篇文章简单带大家看了下 Bluetooth 这个库的使用方式及源码,大家可以自己加一下这个库的依赖,然后就可以直接看它的源码了。

dependencies {
    implementation("androidx.bluetooth:bluetooth:1.0.0-alpha02")
}

2024,越来越卷,活越来越多,抽出空来写一篇文章不容易,正好是工作内容和这个有些相关,故了解了下。祝大家安好吧,少卷。