@@ -4,12 +4,19 @@ import android.bluetooth.BluetoothGatt
44import android.bluetooth.BluetoothGatt.GATT_SUCCESS
55import com.juul.kable.gatt.Callback
66import com.juul.kable.gatt.GattStatus
7+ import com.juul.kable.gatt.Response
78import com.juul.kable.logs.Logger
89import com.juul.kable.logs.Logging
910import kotlinx.coroutines.CoroutineDispatcher
11+ import kotlinx.coroutines.CoroutineScope
12+ import kotlinx.coroutines.Deferred
13+ import kotlinx.coroutines.Job
14+ import kotlinx.coroutines.async
15+ import kotlinx.coroutines.cancel
1016import kotlinx.coroutines.sync.Mutex
1117import kotlinx.coroutines.sync.withLock
1218import kotlinx.coroutines.withContext
19+ import kotlin.coroutines.CoroutineContext
1320
1421public class OutOfOrderGattCallbackException internal constructor(
1522 message : String ,
@@ -18,6 +25,7 @@ public class OutOfOrderGattCallbackException internal constructor(
1825private val GattSuccess = GattStatus (GATT_SUCCESS )
1926
2027internal class Connection (
28+ parentCoroutineContext : CoroutineContext ,
2129 internal val bluetoothGatt : BluetoothGatt ,
2230 internal val dispatcher : CoroutineDispatcher ,
2331 private val callback : Callback ,
@@ -29,10 +37,12 @@ internal class Connection(
2937 callback.invokeOnDisconnected(::close)
3038 }
3139
40+ private val scope = CoroutineScope (parentCoroutineContext + Job (parentCoroutineContext[Job ]))
41+
3242 private val logger = Logger (logging, tag = " Kable/Connection" , identifier = bluetoothGatt.device.address)
3343
3444 private val lock = Mutex ()
35- private var pending = false
45+ private var deferredResponse : Deferred < Response > ? = null
3646
3747 /* *
3848 * Executes specified [BluetoothGatt] [action].
@@ -51,28 +61,33 @@ internal class Connection(
5161 suspend inline fun <reified T > execute (
5262 crossinline action : BluetoothGatt .() -> Boolean ,
5363 ): T = lock.withLock {
54- if (pending) {
55- // Discard response as we've performed another `execute` without the previous finishing. This happens if a
56- // previous `execute` was cancelled after invoking GATT action, but before receiving response from callback
57- // channel. See https://github.com/JuulLabs/kable/issues/326 for more details.
58- val response = callback.onResponse.receive()
59- pending = false
60- logger.warn {
61- message = " Discarded response"
62- detail(" response" , response.toString())
64+ deferredResponse?.let {
65+ if (it.isActive) {
66+ // Discard response as we've performed another `execute` without the previous finishing. This happens if
67+ // a previous `execute` was cancelled after invoking GATT action, but before receiving response from
68+ // callback channel. See the following issues for more details:
69+ // https://github.com/JuulLabs/kable/issues/326
70+ // https://github.com/JuulLabs/kable/issues/450
71+ val response = it.await()
72+ logger.warn {
73+ message = " Discarded response"
74+ detail(" response" , response.toString())
75+ }
6376 }
6477 }
6578
6679 withContext(dispatcher) {
67- pending = true
68- action.invoke(bluetoothGatt) || throw GattRequestRejectedException ()
80+ if (! bluetoothGatt.action()) throw GattRequestRejectedException ()
6981 }
82+ val deferred = scope.async { callback.onResponse.receive() }
83+ deferredResponse = deferred
7084
7185 val response = try {
72- callback.onResponse.receive(). also { pending = false }
86+ deferred.await()
7387 } catch (e: ConnectionLostException ) {
7488 throw ConnectionLostException (cause = e)
7589 }
90+ deferredResponse = null
7691
7792 if (response.status != GattSuccess ) throw GattStatusException (response.toString())
7893
@@ -110,6 +125,7 @@ internal class Connection(
110125 }
111126
112127 fun close () {
128+ scope.cancel()
113129 bluetoothGatt.close()
114130 invokeOnClose.invoke()
115131 }
0 commit comments