I am new to Android development and concurrency and I am trying to build a Kotlin app, where it connects to a BLE device and reads characteristics using readCharacteristic
function and use the value read. The issue is, I want the main thread (or whatever thread the readCharacteristic
is called in) to block and wait for a the callback. This is because I need this value to be displayed and used for calculation.
Currently all BLE operations are put in a queue so that only one operation will do work at a time. The queue is implemented as the following.
object ConnectionManager {
private val deviceGattMap = ConcurrentHashMap<BluetoothDevice, BluetoothGatt>()
private val operationQueue = ConcurrentLinkedQueue<BleOperationType>()
private var pendingOperation: BleOperationType? = null
private var connecting = false
@Synchronized
private fun enqueueOperation(operation: BleOperationType) {
operationQueue.add(operation)
if (pendingOperation == null) {
doNextOperation()
}
}
@Synchronized
private fun signalEndOfOperation() {
pendingOperation = null
if (operationQueue.isNotEmpty()) {
doNextOperation()
}
}
@SuppressLint("MissingPermission")
@Synchronized
private fun doNextOperation() {
if (pendingOperation != null) {
Log.e(
"ConnectionManager",
"doNextOperation() called when an operation is pending! Aborting."
)
return
}
val operation = operationQueue.poll() ?: run {
Log.i("ConnectionManager", "Operation queue empty, returning")
return
}
pendingOperation = operation
if (operation is Connect) {
connecting = true
operation.device.connectGatt(operation.context, false, gattCallback)
while (connecting) {
}
return
}
val gatt = deviceGattMap[operation.device]
?: [email protected] {
Log.e(
"ConnectionManager",
"Not connected to ${operation.device.address}! Aborting $operation operation."
)
signalEndOfOperation()
return
}
when (operation) {
is Disconnect -> {
gatt.close()
deviceGattMap.remove(operation.device)
signalEndOfOperation()
}
is CharacteristicWrite -> {
gatt.findCharacteristic(operation.characteristicUuid)?.let { characteristic ->
characteristic.value = operation.value
characteristic.writeType = operation.writeType
gatt.writeCharacteristic(characteristic)
} ?: run {
Log.e(
"ConnectionManager",
"Characteristic ${operation.characteristicUuid} not found!"
)
signalEndOfOperation()
}
}
is CharacteristicRead -> {
gatt.findCharacteristic(operation.characteristicUuid)?.let { characteristic ->
gatt.readCharacteristic(characteristic)
} ?: [email protected] {
Log.e(
"ConnectionManager",
"Characteristic ${operation.characteristicUuid} not found!"
)
signalEndOfOperation()
}
}
is WriteDescriptor -> {
.......
}
} is ......
}
}
This is the GATT callback:
@SuppressLint("MissingPermission")
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
Log.w("BluetoothGattCallback", "Connection state changed: $status")
if (status == BluetoothGatt.GATT_SUCCESS) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
deviceGattMap[gatt.device] = gatt
Handler(Looper.getMainLooper()).post {
deviceGattMap[gatt.device]?.discoverServices()
}
connecting = false
} else if (newState == BluetoothProfile.STATE_DISCONNECTING) {
disconnect(gatt.device)
}
} else {
connecting = false
Log.w("BluetoothGattCallback", "Error $status")
if (pendingOperation is Connect) {
signalEndOfOperation()
}
//disconnect(gatt.device)
} // Todo handle errors
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
with(gatt) {
if (status == BluetoothGatt.GATT_SUCCESS) {
Log.w("BluetoothGattCallback", "Discovered ${services.size} services")
if (services.isEmpty()) {
Log.i(
"PrintGattTable",
"No services characteristics available, Call DiscoverServices() first?"
)
return
}
else {
services.forEach { service ->
val characteristicsTable = service.characteristics.joinToString(
separator = "\n | --",
prefix = "| --"
) {
it.uuid.toString()
}
Log.i(
"PrintGattTable",
"\n Services ${service.uuid} \n Characteristics ${characteristicsTable}"
)
}
}
ConnectionManager.requestMtu(gatt.device, 517)
} else {
Log.e("BluetoothGattCallback", "Service discovery failed with status $status")
disconnect(device)
}
}
if (pendingOperation is Connect) {
signalEndOfOperation()
}
}
override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
Log.i("MTU", "ATT MTU changed to $mtu, success ${status == BluetoothGatt.GATT_SUCCESS}")
if (pendingOperation is MtuRequest) {
signalEndOfOperation()
}
}
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
with(characteristic) {
when (status) {
BluetoothGatt.GATT_SUCCESS -> {
if (characteristic.uuid.toString() == BatteryCharacteristicUUID) {
Log.i("BluetoothGattCallback", "Read characteristic $uuid:\n${
value.joinToString(
separator = " ",
prefix = "0x"
) { String.format("%02X", it) }
} and \n ${value[0].toUInt()}"
)
}
else {
var light : MutableList<UInt> = mutableListOf()
for (i in 0..value.size-1 step 4) {
light.add(
((value[i].toUInt() and 0xFFu) shl 24) or
((value[i + 1].toUInt() and 0xFFu) shl 16) or
((value[i + 2].toUInt() and 0xFFu) shl 8) or
(value[i+ 3].toUInt() and 0xFFu))
}
Log.i("BluetoothGattCallback", "Read characteristic $uuid:\n${
value.joinToString(
separator = " ",
prefix = "0x"
) { String.format("%02X", it) }
}" +
" and \n ${light}"
)
}
}
BluetoothGatt.GATT_READ_NOT_PERMITTED -> {
}
else -> {
Log.e("BluetoothGattCallback", "Characteristic read failed for $uuid, error: $status")
}
}
}
if (pendingOperation is CharacteristicRead) {
signalEndOfOperation()
}
}
}
When to read the characteristics I call ConnectionManager.readCharacteristic()
which in turn checks if characteristic is readable and if device is connected then adds the operation to the queue.
When calling
ConnectionManager.readCharacteristic(device, char)
Log.i("Tag", $char.value)
The output is null since the Log statement is executed before the callback for the read is executed.
Is there a way to block the thread without time delay or maybe it is not a good idea to block and should look into a solution involving future or promises? If so, any suggestion is welcome!
In my opinion, if possible, it is better to follow the guidelines suggested by the Google site, i.e., to use a service and notify the activity with Broadcast updates.
For details, see my answer How do I connect two android devices instantaneously based on proximity?.