From 07498db173961899276c61b4c53bce2eabd837e0 Mon Sep 17 00:00:00 2001 From: Rohit Sangwan Date: Fri, 3 Jan 2025 11:29:00 +0000 Subject: [PATCH] Improve Android bluetoothGatt management (#124) * Improve Android bluetoothGatt management * Update Changelog * Apply suggestions from code review Co-authored-by: Foti Dim --------- Co-authored-by: Foti Dim --- CHANGELOG.md | 3 +- .../universal_ble/UniversalBleHelper.kt | 28 ++++++++---- .../universal_ble/UniversalBlePlugin.kt | 44 +++++++++---------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00ba0ca..f96ce7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt index 466defe..265bd78 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBleHelper.kt @@ -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() +private val knownGatts = mutableMapOf() val ccdCharacteristic: UUID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb") enum class BleConnectionState(val value: Long) { @@ -91,11 +85,29 @@ fun List.toUUIDList(): List { 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 diff --git a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt index 322ce95..3442916 100644 --- a/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt +++ b/android/src/main/kotlin/com/navideck/universal_ble/UniversalBlePlugin.kt @@ -48,7 +48,6 @@ class UniversalBlePlugin : UniversalBlePlatformChannel, BluetoothGattCallback(), private lateinit var bluetoothManager: BluetoothManager private lateinit var safeScanner: SafeScanner private val cachedServicesMap = mutableMapOf>() - private val devicesStateMap = mutableMapOf() 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>) -> 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"