-
Notifications
You must be signed in to change notification settings - Fork 76
/
Connection.kt
83 lines (70 loc) · 3.04 KB
/
Connection.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.juul.kable
import android.bluetooth.BluetoothGatt
import android.bluetooth.BluetoothGatt.GATT_SUCCESS
import com.juul.kable.gatt.Callback
import com.juul.kable.gatt.GattStatus
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
public class OutOfOrderGattCallbackException internal constructor(
message: String,
) : IllegalStateException(message)
internal data class CharacteristicChange(
val characteristic: Characteristic,
val data: ByteArray,
)
private val GattSuccess = GattStatus(GATT_SUCCESS)
internal class Connection(
internal val bluetoothGatt: BluetoothGatt,
private val dispatcher: CoroutineDispatcher,
private val callback: Callback,
private val invokeOnClose: () -> Unit,
) {
init {
callback.invokeOnDisconnected(::close)
}
val characteristicChanges = callback.onCharacteristicChanged
.map { (bluetoothGattCharacteristic, value) ->
CharacteristicChange(bluetoothGattCharacteristic.toLazyCharacteristic(), value)
}
private val lock = Mutex()
/**
* Executes specified [BluetoothGatt] [action].
*
* Android Bluetooth Low Energy has strict requirements: all I/O must be executed sequentially. In other words, the
* response for an [action] must be received before another [action] can be performed. Additionally, the Android BLE
* stack can be unstable if I/O isn't performed on a dedicated thread.
*
* These requirements are fulfilled by ensuring that all [action]s are performed behind a [Mutex]. On Android pre-O
* a single threaded [CoroutineDispatcher] is used, Android O and newer a [CoroutineDispatcher] backed by an Android
* `Handler` is used (and is also used in the Android BLE [Callback]).
*
* @throws GattRequestRejectedException if underlying `BluetoothGatt` method call returns `false`.
* @throws GattStatusException if response has a non-`GATT_SUCCESS` status.
*/
suspend inline fun <reified T> execute(
crossinline action: BluetoothGatt.() -> Boolean,
): T = lock.withLock {
withContext(dispatcher) {
action.invoke(bluetoothGatt) || throw GattRequestRejectedException()
}
val response = try {
callback.onResponse.receive()
} catch (e: ConnectionLostException) {
throw ConnectionLostException(cause = e)
}
if (response.status != GattSuccess) throw GattStatusException(response.toString())
// `lock` should always enforce a 1:1 matching of request to response, but if an Android `BluetoothGattCallback`
// method gets called out of order then we'll cast to the wrong response type.
response as? T
?: throw OutOfOrderGattCallbackException(
"Unexpected response type ${response.javaClass.simpleName} received"
)
}
fun close() {
bluetoothGatt.close()
invokeOnClose.invoke()
}
}