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

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

Jetpack

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

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

bluetooth

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

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 版本,但也可以去看看源码嘛!
blue

很显然,全是 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: BluetoothAdapteroverride 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.deviceval deviceName = device.name ?: "Unknown"val deviceAddress = device.addressval rssi = result.rssiLog.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.addressLog.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@connectGattval result = readCharacteristic(batteryLevel)if (result.isSuccess) {val value = result.getOrNull() ?: return@connectGattval battery = value[0].toInt() and 0xffLog.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?.adaptervar 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 Advertisefun 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: FwkScanSettingsfun 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@DoNotInlinefun serviceSolicitationUuids(fwkScanResult: FwkScanResult): List<ParcelUuid> =fwkScanResult.scanRecord?.serviceSolicitationUuids.orEmpty()}@RequiresApi(26)private object ScanResultApi26Impl {@JvmStatic@DoNotInlinefun isConnectable(fwkScanResult: FwkScanResult): Boolean =fwkScanResult.isConnectable@JvmStatic@DoNotInlinefun 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: Longget() = 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,126val rssi: Intget() = fwkScanResult.rssi// 返回周期广告间隔,单位为毫秒,取值范围是7.5ms ~ 81918.75msval periodicAdvertisingInterval: Longget() = 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,越来越卷,活越来越多,抽出空来写一篇文章不容易,正好是工作内容和这个有些相关,故了解了下。祝大家安好吧,少卷。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/796853.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

专题【双指针】【学习题】刷题日记

题目列表 11. 盛最多水的容器 42. 接雨水 15. 三数之和 16. 最接近的三数之和 18. 四数之和 26. 删除有序数组中的重复项 27. 移除元素 75. 颜色分类 167. 两数之和 II - 输入有序数组 2024.04.06 11. 盛最多水的容器 题目 给定一个长度为 n 的整数数组 height 。有 n 条垂…

PHP实现网站微信扫码关注公众号后自动注册登陆实现方法及代码【关注收藏】

在网站注册登陆这环节&#xff0c;增加微信扫码注册登陆&#xff0c;普通的方法需要开通微信开发者平台&#xff0c;生成二维码扫码后才能获取用户的uinonid或openid&#xff0c;实现注册登陆&#xff0c;但这样比较麻烦还要企业认证交费开发者平台&#xff0c;而且没有和公众号…

【SQL】1890. 2020年最后一次登录(简单写法;窗口函数写法)

前述 sql 中 between 的边界问题 ---- between 边界&#xff1a;闭区间&#xff0c;not between 边界&#xff1a;开区间 在 sql 中&#xff0c; between 边界&#xff1a;闭区间not between 边界&#xff1a;开区间 题目描述 leetcode题目&#xff1a;1890. 2020年最后一…

【leetcode面试经典150题】16.接雨水(C++)

【leetcode面试经典150题】专栏系列将为准备暑期实习生以及秋招的同学们提高在面试时的经典面试算法题的思路和想法。本专栏将以一题多解和精简算法思路为主&#xff0c;题解使用C语言。&#xff08;若有使用其他语言的同学也可了解题解思路&#xff0c;本质上语法内容一致&…

aardio教程五) 写Python风格的aardio代码(字符串篇)

前言 熟悉一个新的语言最麻烦的就是需要了解一些库的使用&#xff0c;特别是基础库的使用。 所以我想给aardio封装一个Python风格的库&#xff0c;Python里的基础库是什么方法名&#xff0c;aardio里也封装同样的方法名。 这样就不需要单独去了解aardio里一些方法的使用细节…

Lanelets_ 高效的自动驾驶地图表达方式

Lanelets: 高效的自动驾驶地图表达方式 附赠自动驾驶学习资料和量产经验&#xff1a;链接 LaneLets是自动驾驶领域高精度地图的一种高效表达方式&#xff0c;它以彼此相互连接的LaneLets来描述自动驾驶可行驶区域&#xff0c;不仅可以表达车道几何&#xff0c;也可以完整表述车…

AIGC实战——ProGAN(Progressive Growing Generative Adversarial Network)

AIGC实战——ProGAN 0. 前言1. ProGAN2. 渐进式训练3. 其他技术3.1 小批标准差3.2 均等学习率3.3 逐像素归一化 4. 图像生成小结系列链接 0. 前言 我们已经学习了使用生成对抗网络 (Generative Adversarial Network, GAN) 解决各种图像生成任务。GAN 的模型架构和训练过程具有…

真实的招生办对话邮件及美国高校官网更新的反 AI 政策

这两年 ChatGPT 的热度水涨船高&#xff0c;其编写功能强大&#xff0c;且具备强大的信息整合效果&#xff0c;所以呈现的内容在一定程度上具备可读性。 那么&#xff0c;美国留学文书可以用 ChatGPT 写吗&#xff1f;使用是否有风险&#xff1f;外网博主 Kushi Uppu 在这个申…

基于卷积神经网络的天气识别系统(pytorch框架)【python源码+UI界面+前端界面+功能源码详解】

功能演示&#xff1a; 天气识别系统&#xff0c;vgg16&#xff0c;mobilenet卷积神经网络&#xff08;pytorch框架&#xff09;_哔哩哔哩_bilibili &#xff08;一&#xff09;简介 基于卷积神经网络的天气识别系统是在pytorch框架下实现的&#xff0c;系统中有两个模型可选…

vue+elementUI实现表格组件的封装

效果图&#xff1a; 在父组件使用表格组件 <table-listref"table":stripe"true":loading"loading":set-table-h"slotProps.setMainCardBodyH":table-data"tableData":columns"columns.tableList || []":ra…

基于Springboot的Java学习平台

采用技术 基于Springbootjava学习平台的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 系统功能模块 后台管理 用户注册 课程信息 作业信息 资料信息…

电工技术学习笔记——正弦交流电路

一、正弦交流电路 1. 正弦量的向量表示法 向量表示方法&#xff1a;正弦交流电路中&#xff0c;相量表示法是一种常用的方法&#xff0c;用于描述电压、电流及其相位关系。相量表示法将正弦交流信号表示为复数&#xff0c;通过复数的运算来描述电路中各种参数的相互关系 …

java中的正则表达式和异常

正则表达式&#xff1a; 作用一&#xff1a;用来校验数据格式是否合法 作用二&#xff1a;在文本中查找满足要求的内容 不用正则表达式&#xff1a;检验QQ号是否合法&#xff0c;要求全部是数字&#xff0c;长度在6-20&#xff0c;不能以0开头 public class test {public stat…

【Linux实践室】Linux高级用户管理实战指南:创建与删除用户组操作详解

&#x1f308;个人主页&#xff1a;聆风吟_ &#x1f525;系列专栏&#xff1a;Linux实践室、网络奇遇记 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 一. ⛳️任务描述二. ⛳️相关知识2.1 &#x1f514;Linux创建用户组命令2.1.1 知识点讲解2.1.2…

亲手开发全国海域潮汐表查询微信小程序详情教程及代码

最近在做一个全国海域潮汐表查询&#xff0c;可以为赶海钓鱼爱好者提供涨潮退潮时间表及潮高信息。 下面教大家怎么做一个这样的小程序。 主要功能&#xff0c;根据IP定位地理位置&#xff0c;自动查询出省份或城市的港口&#xff0c;进入后预测7天内港口潮汐表查询。 步骤&…

全坚固笔记本丨工业笔记本丨三防笔记本相较于普通笔记本有哪些优势?

三防笔记本和普通笔记本在设计和性能方面存在显著差异&#xff0c;三防笔记本相较于普通笔记本具备以下优势&#xff1a; 三防笔记本通常采用耐磨、耐摔的材料&#xff0c;并具有坚固的外壳设计&#xff0c;能够承受恶劣环境和意外碰撞&#xff0c;有效保护内部组件不受损坏。相…

【Linux】进程初步理解

个人主页 &#xff1a; zxctscl 如有转载请先通知 文章目录 1. 冯诺依曼体系结构1.1 认识冯诺依曼体系结构1.2 存储金字塔 2. 操作系统2.1 概念2.2 结构2.3 操作系统的管理 3. 进程3.1 进程描述3.2 Linux下的PCB 4. task_struct本身内部属性4.1 启动4.2 进程的创建方式4.2.1 父…

C/C++预处理过程

目录 前言&#xff1a; 1. 预定义符号 2. #define定义常量 3. #define定义宏 4. 带有副作用的宏参数 5. 宏替换的规则 6. 宏和函数的对比 7. #和## 8. 命名约定 9. #undef 10. 命令行定义 11. 条件编译 12. 头文件的包含 13. 其他预处理指令 总结&#x…

谷歌在生成式人工智能领域的挑战与机遇:内部纷争与市场压力下的战略调整

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

nest获取传入接口的参数

代码 Query 可接收接口路径中传入的参数 Body 可接收body中传入的参数 Headers 可接收Headers中传入的参数 import { Controller, Post, Get, Body, Query, Headers } from nestjs/common;// 定义getList参数类型 export class ListDto {readonly page: number;readonly page…