From ccba4636861e5a99b8b2af8c09bf8fb58e40afd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Apr 2026 02:16:15 +0000 Subject: [PATCH] fix(ble): migrate AndroidBleTransport off deprecated BLE APIs Replaces deprecated BluetoothGattCharacteristic.value, writeCharacteristic, notifyCharacteristicChanged, writeDescriptor and connectGatt overloads with their Tiramisu (API 33+) replacements, falling back to the legacy API with @Suppress("DEPRECATION") when running on older devices. Adds the new onCharacteristicChanged(gatt, characteristic, value) overload and caches the last notified payload so GATT read requests no longer read from the deprecated characteristic.value field. --- .../nipBEBle/transport/AndroidBleTransport.kt | 68 ++++++++++++++++--- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt index 640670baa..51b94b5dc 100644 --- a/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt +++ b/quartz/src/androidMain/kotlin/com/vitorpamplona/quartz/nipBEBle/transport/AndroidBleTransport.kt @@ -32,6 +32,7 @@ import android.bluetooth.BluetoothGattServerCallback import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothProfile +import android.bluetooth.BluetoothStatusCodes import android.bluetooth.le.AdvertiseCallback import android.bluetooth.le.AdvertiseData import android.bluetooth.le.AdvertiseSettings @@ -40,6 +41,7 @@ import android.bluetooth.le.ScanFilter import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanSettings import android.content.Context +import android.os.Build import android.os.ParcelUuid import com.vitorpamplona.quartz.nipBEBle.AndroidBleTransportContract import com.vitorpamplona.quartz.nipBEBle.BleConfig @@ -89,6 +91,11 @@ class AndroidBleTransport( private var readCharacteristic: BluetoothGattCharacteristic? = null + // Caches the most recently notified payload so that GATT read requests on the + // read characteristic can return a meaningful value without relying on the + // deprecated BluetoothGattCharacteristic.value field. + private var lastNotifiedValue: ByteArray = ByteArray(0) + /** * Sets the listener for transport events. Must be called before [startAdvertising] or [startScanning]. */ @@ -206,7 +213,14 @@ class AndroidBleTransport( } peerMap[peer.deviceUuid] = peer - val gatt = device.connectGatt(context, false, gattClientCallback, BluetoothDevice.TRANSPORT_LE) + val gatt = + device.connectGatt( + context, + false, + gattClientCallback, + BluetoothDevice.TRANSPORT_LE, + BluetoothDevice.PHY_LE_1M_MASK, + ) gattConnections[peer.deviceUuid] = gatt } @@ -227,8 +241,18 @@ class AndroidBleTransport( val service = gatt.getService(serviceUuid) ?: return false val char = service.getCharacteristic(writeCharUuid) ?: return false - char.value = chunk - return gatt.writeCharacteristic(char) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeCharacteristic( + char, + chunk, + BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT, + ) == BluetoothStatusCodes.SUCCESS + } else { + @Suppress("DEPRECATION") + char.value = chunk + @Suppress("DEPRECATION") + gatt.writeCharacteristic(char) + } } override fun notifyChunk( @@ -239,8 +263,15 @@ class AndroidBleTransport( val device = connectedDevices[peer.deviceUuid] ?: return false val char = readCharacteristic ?: return false - char.value = chunk - return server.notifyCharacteristicChanged(device, char, false) + lastNotifiedValue = chunk + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + server.notifyCharacteristicChanged(device, char, false, chunk) == BluetoothStatusCodes.SUCCESS + } else { + @Suppress("DEPRECATION") + char.value = chunk + @Suppress("DEPRECATION") + server.notifyCharacteristicChanged(device, char, false) + } } override fun requestMtu( @@ -341,7 +372,7 @@ class AndroidBleTransport( requestId, BluetoothGatt.GATT_SUCCESS, 0, - characteristic.value ?: ByteArray(0), + lastNotifiedValue, ) } } @@ -398,13 +429,20 @@ class AndroidBleTransport( val cccd = readChar.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) cccd?.let { - it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE - gatt.writeDescriptor(it) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + gatt.writeDescriptor(it, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE) + } else { + @Suppress("DEPRECATION") + it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE + @Suppress("DEPRECATION") + gatt.writeDescriptor(it) + } } listener?.onPeerConnected(peer) } + @Deprecated("Used on Android 12 and lower. API 33+ uses the overload with the value parameter.") override fun onCharacteristicChanged( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, @@ -412,12 +450,26 @@ class AndroidBleTransport( if (characteristic.uuid == readCharUuid) { val peerUuid = deviceUuidMap[gatt.device.address] ?: return val peer = peerMap[peerUuid] ?: return + @Suppress("DEPRECATION") val value = characteristic.value ?: return listener?.onChunkReceived(peer, value) } } + override fun onCharacteristicChanged( + gatt: BluetoothGatt, + characteristic: BluetoothGattCharacteristic, + value: ByteArray, + ) { + if (characteristic.uuid == readCharUuid) { + val peerUuid = deviceUuidMap[gatt.device.address] ?: return + val peer = peerMap[peerUuid] ?: return + + listener?.onChunkReceived(peer, value) + } + } + override fun onCharacteristicWrite( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic,