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.
This commit is contained in:
Claude
2026-04-15 02:16:15 +00:00
parent b10a7a5153
commit ccba463686
@@ -32,6 +32,7 @@ import android.bluetooth.BluetoothGattServerCallback
import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager import android.bluetooth.BluetoothManager
import android.bluetooth.BluetoothProfile import android.bluetooth.BluetoothProfile
import android.bluetooth.BluetoothStatusCodes
import android.bluetooth.le.AdvertiseCallback import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings import android.bluetooth.le.AdvertiseSettings
@@ -40,6 +41,7 @@ import android.bluetooth.le.ScanFilter
import android.bluetooth.le.ScanResult import android.bluetooth.le.ScanResult
import android.bluetooth.le.ScanSettings import android.bluetooth.le.ScanSettings
import android.content.Context import android.content.Context
import android.os.Build
import android.os.ParcelUuid import android.os.ParcelUuid
import com.vitorpamplona.quartz.nipBEBle.AndroidBleTransportContract import com.vitorpamplona.quartz.nipBEBle.AndroidBleTransportContract
import com.vitorpamplona.quartz.nipBEBle.BleConfig import com.vitorpamplona.quartz.nipBEBle.BleConfig
@@ -89,6 +91,11 @@ class AndroidBleTransport(
private var readCharacteristic: BluetoothGattCharacteristic? = null 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]. * Sets the listener for transport events. Must be called before [startAdvertising] or [startScanning].
*/ */
@@ -206,7 +213,14 @@ class AndroidBleTransport(
} }
peerMap[peer.deviceUuid] = peer 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 gattConnections[peer.deviceUuid] = gatt
} }
@@ -227,8 +241,18 @@ class AndroidBleTransport(
val service = gatt.getService(serviceUuid) ?: return false val service = gatt.getService(serviceUuid) ?: return false
val char = service.getCharacteristic(writeCharUuid) ?: return false val char = service.getCharacteristic(writeCharUuid) ?: return false
char.value = chunk return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
return gatt.writeCharacteristic(char) gatt.writeCharacteristic(
char,
chunk,
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT,
) == BluetoothStatusCodes.SUCCESS
} else {
@Suppress("DEPRECATION")
char.value = chunk
@Suppress("DEPRECATION")
gatt.writeCharacteristic(char)
}
} }
override fun notifyChunk( override fun notifyChunk(
@@ -239,8 +263,15 @@ class AndroidBleTransport(
val device = connectedDevices[peer.deviceUuid] ?: return false val device = connectedDevices[peer.deviceUuid] ?: return false
val char = readCharacteristic ?: return false val char = readCharacteristic ?: return false
char.value = chunk lastNotifiedValue = chunk
return server.notifyCharacteristicChanged(device, char, false) 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( override fun requestMtu(
@@ -341,7 +372,7 @@ class AndroidBleTransport(
requestId, requestId,
BluetoothGatt.GATT_SUCCESS, BluetoothGatt.GATT_SUCCESS,
0, 0,
characteristic.value ?: ByteArray(0), lastNotifiedValue,
) )
} }
} }
@@ -398,13 +429,20 @@ class AndroidBleTransport(
val cccd = readChar.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")) val cccd = readChar.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"))
cccd?.let { cccd?.let {
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
gatt.writeDescriptor(it) gatt.writeDescriptor(it, BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE)
} else {
@Suppress("DEPRECATION")
it.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
@Suppress("DEPRECATION")
gatt.writeDescriptor(it)
}
} }
listener?.onPeerConnected(peer) listener?.onPeerConnected(peer)
} }
@Deprecated("Used on Android 12 and lower. API 33+ uses the overload with the value parameter.")
override fun onCharacteristicChanged( override fun onCharacteristicChanged(
gatt: BluetoothGatt, gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic, characteristic: BluetoothGattCharacteristic,
@@ -412,12 +450,26 @@ class AndroidBleTransport(
if (characteristic.uuid == readCharUuid) { if (characteristic.uuid == readCharUuid) {
val peerUuid = deviceUuidMap[gatt.device.address] ?: return val peerUuid = deviceUuidMap[gatt.device.address] ?: return
val peer = peerMap[peerUuid] ?: return val peer = peerMap[peerUuid] ?: return
@Suppress("DEPRECATION")
val value = characteristic.value ?: return val value = characteristic.value ?: return
listener?.onChunkReceived(peer, value) 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( override fun onCharacteristicWrite(
gatt: BluetoothGatt, gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic, characteristic: BluetoothGattCharacteristic,