Improve Android bluetoothGatt management (#124)

* Improve Android bluetoothGatt management

* Update Changelog

* Apply suggestions from code review

Co-authored-by: Foti Dim <foti@navideck.com>

---------

Co-authored-by: Foti Dim <foti@navideck.com>
This commit is contained in:
Rohit Sangwan
2025-01-03 11:29:00 +00:00
committed by GitHub
parent 8b1a21939b
commit 07498db173
3 changed files with 44 additions and 31 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
## 0.14.0
## 0.15.0
* `getSystemDevices(withServices:)` now sets several generic services by default as filter
* `getConnectionState` on Android will now return `BleConnectionState.disconnected` if device is connected to the system but not to the app
## 0.14.0
* BREAKING CHANGE: `bleDevice.name` now filters out non-printable characters
@@ -1,11 +1,9 @@
package com.navideck.universal_ble
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattDescriptor
import android.bluetooth.BluetoothStatusCodes
import android.bluetooth.le.ScanCallback.SCAN_FAILED_ALREADY_STARTED
import android.bluetooth.le.ScanCallback.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED
@@ -13,20 +11,16 @@ import android.bluetooth.le.ScanCallback.SCAN_FAILED_FEATURE_UNSUPPORTED
import android.bluetooth.le.ScanCallback.SCAN_FAILED_INTERNAL_ERROR
import android.bluetooth.le.ScanCallback.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES
import android.bluetooth.le.ScanCallback.SCAN_FAILED_SCANNING_TOO_FREQUENTLY
import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult
import android.os.Build
import android.os.ParcelUuid
import android.util.Log
import android.util.SparseArray
import androidx.core.util.keyIterator
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.UUID
private const val TAG = "UniversalBlePlugin"
val knownGatts = mutableListOf<BluetoothGatt>()
private val knownGatts = mutableMapOf<String, BluetoothGatt>()
val ccdCharacteristic: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
enum class BleConnectionState(val value: Long) {
@@ -91,11 +85,29 @@ fun List<String>.toUUIDList(): List<UUID> {
return this.map { UUID.fromString(it.validFullUUID()) }
}
fun String.toBluetoothGatt(): BluetoothGatt {
return knownGatts.find { it.device.address == this }
return this.findGatt()
?: throw FlutterError("IllegalArgument", "Unknown deviceId: $this", null)
}
fun String.isKnownGatt(): Boolean {
return this.findGatt() != null
}
fun String.findGatt(): BluetoothGatt? {
return knownGatts[this]
}
fun BluetoothGatt.saveCacheIfNeeded() {
knownGatts[this.device.address] = this
}
fun BluetoothGatt.removeCache() {
knownGatts.remove(this.device.address)
}
fun Int.toAvailabilityState(): Long {
return when (this) {
BluetoothAdapter.STATE_OFF -> AvailabilityState.PoweredOff.value
@@ -48,7 +48,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
private lateinit var bluetoothManager: BluetoothManager
private lateinit var safeScanner: SafeScanner
private val cachedServicesMap = mutableMapOf<String, List<String>>()
private val devicesStateMap = mutableMapOf<String, Int>()
private val universalBleFilterUtil = UniversalBleFilterUtil()
// Flutter Futures
@@ -182,11 +181,10 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
override fun connect(deviceId: String) {
val currentState = devicesStateMap[deviceId]
// If already connected, send connected message,
// if connecting, do nothing
knownGatts.find { it.device.address == deviceId }?.let {
deviceId.findGatt()?.let {
val currentState = bluetoothManager.getConnectionState(it.device, BluetoothProfile.GATT)
if (currentState == BluetoothGatt.STATE_CONNECTED) {
Log.e(TAG, "$deviceId Already connected")
mainThreadHandler?.post {
@@ -210,7 +208,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
} else {
remoteDevice.connectGatt(context, false, this)
}
knownGatts.add(gatt)
gatt.saveCacheIfNeeded()
}
override fun disconnect(deviceId: String) {
@@ -218,10 +216,18 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
override fun getConnectionState(deviceId: String): Long {
return bluetoothManager.getConnectionState(
val connectionState = bluetoothManager.getConnectionState(
bluetoothManager.adapter.getRemoteDevice(deviceId),
BluetoothProfile.GATT
).toBleConnectionState().value
)
return if (deviceId.isKnownGatt() || connectionState == BluetoothGatt.STATE_DISCONNECTED || connectionState == BluetoothGatt.STATE_DISCONNECTING) {
connectionState.toBleConnectionState().value
} else {
// Might be connected with device, but not with app
Log.e(TAG, "Device might be connected but not known to this app")
BleConnectionState.Disconnected.value
}
}
override fun discoverServices(
@@ -229,19 +235,14 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
callback: (Result<List<UniversalBleService>>) -> Unit,
) {
try {
if (!deviceId.toBluetoothGatt().discoverServices()) {
val gatt = deviceId.toBluetoothGatt()
if (gatt.discoverServices()) {
discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback))
} else {
callback(
Result.failure(
FlutterError(
"Failed",
"Failed to discover services",
null
)
)
Result.failure(FlutterError("Failed", "Failed to discover services", null))
)
return
}
discoverServicesFutureList.add(DiscoverServicesFuture(deviceId, callback))
} catch (e: FlutterError) {
callback(Result.failure(e))
}
@@ -684,7 +685,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
// If its a known gatt, just discover services
knownGatts.find { it.device.address == device.address }?.let { gatt ->
device.address.findGatt()?.let { gatt ->
gatt.services?.let { services ->
updateCallback(services.map { service -> service.uuid.toString() })
return
@@ -710,7 +711,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothGatt.STATE_CONNECTED) {
if (gatt?.discoverServices() != true) {
updateCallback(null)
if (!knownGatts.any { it.device.address == device.address }) {
if (!device.address.isKnownGatt()) {
gatt?.disconnect()
}
}
@@ -727,7 +728,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
updateCallback(uuids)
}
if (!knownGatts.any { it.device.address == device.address }) {
if (!device.address.isKnownGatt()) {
gatt?.disconnect()
}
}
@@ -741,7 +742,7 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
}
private fun cleanConnection(gatt: BluetoothGatt) {
knownGatts.remove(gatt)
gatt.removeCache()
gatt.disconnect()
readResultFutureList.removeAll {
@@ -892,7 +893,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(),
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
devicesStateMap[gatt.device.address] = newState
Log.d(
TAG,
"onConnectionStateChange-> Status: $status ${status.parseHciErrorCode()}, NewState: $newState"