Skip to content
This repository was archived by the owner on Aug 30, 2022. It is now read-only.

Commit 6df9fd4

Browse files
committed
Remove Messenger actor and use withContext instead
Inspired by Kotlin/kotlinx.coroutines#87 [comment] by elizarov on Jun 15: > when you ask and actor and want a result back the proper design would > be to have a `suspend fun` with a normal (non-deferred) `Result`. > However, please note that this whole ask & wait pattern is an > anti-pattern in actor-based systems, since it limits scalability. [comment]: Kotlin/kotlinx.coroutines#87 (comment)
1 parent 790dc5f commit 6df9fd4

File tree

13 files changed

+158
-285
lines changed

13 files changed

+158
-285
lines changed

core/src/main/java/ConnectionStateMonitor.kt

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package com.juul.able.experimental
88

99
import android.bluetooth.BluetoothGatt
1010
import android.bluetooth.BluetoothProfile
11-
import com.juul.able.experimental.messenger.OnConnectionStateChange
1211
import kotlinx.coroutines.experimental.channels.ReceiveChannel
1312
import kotlinx.coroutines.experimental.channels.consumeEach
1413
import kotlinx.coroutines.experimental.sync.Mutex

core/src/main/java/CoroutinesDevice.kt

+1-5
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,6 @@ import android.os.RemoteException
1212
import com.juul.able.experimental.ConnectGattResult.Canceled
1313
import com.juul.able.experimental.ConnectGattResult.Failure
1414
import com.juul.able.experimental.ConnectGattResult.Success
15-
import com.juul.able.experimental.messenger.GattCallback
16-
import com.juul.able.experimental.messenger.GattCallbackConfig
17-
import com.juul.able.experimental.messenger.Messenger
1815
import kotlinx.coroutines.experimental.CancellationException
1916

2017
class CoroutinesDevice(
@@ -31,8 +28,7 @@ class CoroutinesDevice(
3128
private fun requestConnectGatt(context: Context, autoConnect: Boolean): CoroutinesGatt? {
3229
val callback = GattCallback(callbackConfig)
3330
val bluetoothGatt = device.connectGatt(context, autoConnect, callback) ?: return null
34-
val messenger = Messenger(bluetoothGatt, callback)
35-
return CoroutinesGatt(bluetoothGatt, messenger)
31+
return CoroutinesGatt(bluetoothGatt, callback)
3632
}
3733

3834
/**

core/src/main/java/CoroutinesGatt.kt

+64-117
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,27 @@ import android.bluetooth.BluetoothGattService
1313
import android.bluetooth.BluetoothProfile.STATE_CONNECTED
1414
import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED
1515
import android.os.RemoteException
16-
import com.juul.able.experimental.messenger.Message.DiscoverServices
17-
import com.juul.able.experimental.messenger.Message.ReadCharacteristic
18-
import com.juul.able.experimental.messenger.Message.RequestMtu
19-
import com.juul.able.experimental.messenger.Message.WriteCharacteristic
20-
import com.juul.able.experimental.messenger.Message.WriteDescriptor
21-
import com.juul.able.experimental.messenger.Messenger
22-
import com.juul.able.experimental.messenger.OnCharacteristicChanged
23-
import com.juul.able.experimental.messenger.OnCharacteristicRead
24-
import com.juul.able.experimental.messenger.OnCharacteristicWrite
25-
import com.juul.able.experimental.messenger.OnConnectionStateChange
26-
import com.juul.able.experimental.messenger.OnDescriptorWrite
27-
import com.juul.able.experimental.messenger.OnMtuChanged
28-
import kotlinx.coroutines.experimental.CompletableDeferred
16+
import kotlinx.coroutines.experimental.CoroutineDispatcher
17+
import kotlinx.coroutines.experimental.ThreadPoolDispatcher
2918
import kotlinx.coroutines.experimental.channels.BroadcastChannel
19+
import kotlinx.coroutines.experimental.channels.ReceiveChannel
20+
import kotlinx.coroutines.experimental.newSingleThreadContext
21+
import kotlinx.coroutines.experimental.withContext
3022
import java.util.UUID
3123

3224
class CoroutinesGatt(
3325
private val bluetoothGatt: BluetoothGatt,
34-
private val messenger: Messenger
26+
private val callback: GattCallback,
27+
private val dispatcher: CoroutineDispatcher = newSingleThreadContext("Gatt")
3528
) : Gatt {
3629

3730
private val connectionStateMonitor by lazy { ConnectionStateMonitor(this) }
3831

3932
override val onConnectionStateChange: BroadcastChannel<OnConnectionStateChange>
40-
get() = messenger.callback.onConnectionStateChange
33+
get() = callback.onConnectionStateChange
4134

4235
override val onCharacteristicChanged: BroadcastChannel<OnCharacteristicChanged>
43-
get() = messenger.callback.onCharacteristicChanged
36+
get() = callback.onCharacteristicChanged
4437

4538
override fun requestConnect(): Boolean = bluetoothGatt.connect()
4639
override fun requestDisconnect(): Unit = bluetoothGatt.disconnect()
@@ -62,7 +55,18 @@ class CoroutinesGatt(
6255
override fun close() {
6356
Able.verbose { "close → Begin" }
6457
connectionStateMonitor.close()
65-
messenger.close()
58+
callback.close()
59+
60+
if (dispatcher is ThreadPoolDispatcher) {
61+
/**
62+
* Explicitly close context (this is needed until #261 is fixed).
63+
*
64+
* [Kotlin Coroutines Issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261)
65+
* [Coroutines actor test Gist](https://gist.github.com/twyatt/c51f81d763a6ee39657233fa725f5435)
66+
*/
67+
dispatcher.close()
68+
}
69+
6670
bluetoothGatt.close()
6771
Able.verbose { "close → End" }
6872
}
@@ -73,52 +77,21 @@ class CoroutinesGatt(
7377
/**
7478
* @throws [RemoteException] if underlying [BluetoothGatt.discoverServices] returns `false`.
7579
*/
76-
override suspend fun discoverServices(): GattStatus {
77-
Able.debug { "discoverServices → send(DiscoverServices)" }
78-
79-
val response = CompletableDeferred<Boolean>()
80-
messenger.send(DiscoverServices(response))
81-
82-
val call = "BluetoothGatt.discoverServices()"
83-
Able.verbose { "discoverServices → Waiting for $call" }
84-
if (!response.await()) {
85-
throw RemoteException("$call returned false.")
80+
override suspend fun discoverServices(): GattStatus =
81+
performBluetoothAction("discoverServices", callback.onServicesDiscovered) {
82+
bluetoothGatt.discoverServices()
8683
}
8784

88-
Able.verbose { "discoverServices → Waiting for BluetoothGattCallback" }
89-
return messenger.callback.onServicesDiscovered.receive().also { status ->
90-
Able.info { "discoverServices, status=${status.asGattStatusString()}" }
91-
}
92-
}
93-
9485
/**
9586
* @throws [RemoteException] if underlying [BluetoothGatt.readCharacteristic] returns `false`.
9687
*/
9788
override suspend fun readCharacteristic(
9889
characteristic: BluetoothGattCharacteristic
99-
): OnCharacteristicRead {
100-
val uuid = characteristic.uuid
101-
Able.debug { "readCharacteristic → send(ReadCharacteristic[uuid=$uuid])" }
102-
103-
val response = CompletableDeferred<Boolean>()
104-
messenger.send(ReadCharacteristic(characteristic, response))
105-
106-
val call = "BluetoothGatt.readCharacteristic(BluetoothGattCharacteristic[uuid=$uuid])"
107-
Able.verbose { "readCharacteristic → Waiting for $call" }
108-
if (!response.await()) {
109-
throw RemoteException("Failed to read characteristic with UUID $uuid.")
90+
): OnCharacteristicRead =
91+
performBluetoothAction("readCharacteristic", callback.onCharacteristicRead) {
92+
bluetoothGatt.readCharacteristic(characteristic)
11093
}
11194

112-
Able.verbose { "readCharacteristic → Waiting for BluetoothGattCallback" }
113-
return messenger.callback.onCharacteristicRead.receive().also { (_, value, status) ->
114-
Able.info {
115-
val bytesString = value.size.bytesString
116-
val statusString = status.asGattStatusString()
117-
"← readCharacteristic $uuid ($bytesString), status=$statusString"
118-
}
119-
}
120-
}
121-
12295
/**
12396
* @param value applied to [characteristic] when characteristic is written.
12497
* @param writeType applied to [characteristic] when characteristic is written.
@@ -128,87 +101,61 @@ class CoroutinesGatt(
128101
characteristic: BluetoothGattCharacteristic,
129102
value: ByteArray,
130103
writeType: WriteType
131-
): OnCharacteristicWrite {
132-
val uuid = characteristic.uuid
133-
Able.debug { "writeCharacteristic → send(WriteCharacteristic[uuid=$uuid])" }
134-
135-
val response = CompletableDeferred<Boolean>()
136-
messenger.send(WriteCharacteristic(characteristic, value, writeType, response))
137-
138-
val call = "BluetoothGatt.writeCharacteristic(BluetoothGattCharacteristic[uuid=$uuid])"
139-
Able.verbose { "writeCharacteristic → Waiting for $call" }
140-
if (!response.await()) {
141-
throw RemoteException("$call returned false.")
104+
): OnCharacteristicWrite =
105+
performBluetoothAction("writeCharacteristic", callback.onCharacteristicWrite) {
106+
characteristic.value = value
107+
characteristic.writeType = writeType
108+
bluetoothGatt.writeCharacteristic(characteristic)
142109
}
143110

144-
Able.verbose { "writeCharacteristic → Waiting for BluetoothGattCallback" }
145-
return messenger.callback.onCharacteristicWrite.receive().also { (_, status) ->
146-
Able.info {
147-
val bytesString = value.size.bytesString
148-
val typeString = writeType.asWriteTypeString()
149-
val statusString = status.asGattStatusString()
150-
"→ writeCharacteristic $uuid ($bytesString), type=$typeString, status=$statusString"
151-
}
152-
}
153-
}
154-
155111
/**
156112
* @param value applied to [descriptor] when descriptor is written.
157113
* @throws [RemoteException] if underlying [BluetoothGatt.writeDescriptor] returns `false`.
158114
*/
159115
override suspend fun writeDescriptor(
160-
descriptor: BluetoothGattDescriptor, value: ByteArray
161-
): OnDescriptorWrite {
162-
val uuid = descriptor.uuid
163-
Able.debug { "writeDescriptor → send(WriteDescriptor[uuid=$uuid])" }
164-
165-
val response = CompletableDeferred<Boolean>()
166-
messenger.send(WriteDescriptor(descriptor, value, response))
167-
168-
val call = "BluetoothGatt.writeDescriptor(BluetoothGattDescriptor[uuid=$uuid])"
169-
Able.verbose { "writeDescriptor → Waiting for $call" }
170-
if (!response.await()) {
171-
throw RemoteException("$call returned false.")
116+
descriptor: BluetoothGattDescriptor,
117+
value: ByteArray
118+
): OnDescriptorWrite =
119+
performBluetoothAction("writeDescriptor", callback.onDescriptorWrite) {
120+
descriptor.value = value
121+
bluetoothGatt.writeDescriptor(descriptor)
172122
}
173123

174-
Able.verbose { "writeDescriptor → Waiting for BluetoothGattCallback" }
175-
return messenger.callback.onDescriptorWrite.receive().also { (_, status) ->
176-
Able.info {
177-
val bytesString = value.size.bytesString
178-
val statusString = status.asGattStatusString()
179-
"→ writeDescriptor $uuid ($bytesString), status=$statusString"
180-
}
181-
}
182-
}
183-
184124
/**
185125
* @throws [RemoteException] if underlying [BluetoothGatt.requestMtu] returns `false`.
186126
*/
187-
override suspend fun requestMtu(mtu: Int): OnMtuChanged {
188-
Able.debug { "requestMtu → send(RequestMtu[mtu=$mtu])" }
189-
190-
val response = CompletableDeferred<Boolean>()
191-
messenger.send(RequestMtu(mtu, response))
192-
193-
val call = "BluetoothGatt.requestMtu($mtu)"
194-
Able.verbose { "requestMtu → Waiting for $call" }
195-
if (!response.await()) {
196-
throw RemoteException("$call returned false.")
127+
override suspend fun requestMtu(mtu: Int): OnMtuChanged =
128+
performBluetoothAction("requestMtu", callback.onMtuChanged) {
129+
bluetoothGatt.requestMtu(mtu)
197130
}
198131

199-
Able.verbose { "requestMtu → Waiting for BluetoothGattCallback" }
200-
return messenger.callback.onMtuChanged.receive().also { (mtu, status) ->
201-
Able.info { "requestMtu $mtu, status=${status.asGattStatusString()}" }
202-
}
203-
}
204-
205132
override fun setCharacteristicNotification(
206133
characteristic: BluetoothGattCharacteristic,
207134
enable: Boolean
208135
): Boolean {
209-
Able.info { "setCharacteristicNotification ${characteristic.uuid} enable=$enable" }
136+
Able.info { "setCharacteristicNotification → uuid=${characteristic.uuid}, enable=$enable" }
210137
return bluetoothGatt.setCharacteristicNotification(characteristic, enable)
211138
}
212-
}
213139

214-
private val Int.bytesString get() = if (this == 1) "$this byte" else "$this bytes"
140+
private suspend fun <T> performBluetoothAction(
141+
methodName: String,
142+
responseChannel: ReceiveChannel<T>,
143+
action: () -> Boolean
144+
): T {
145+
Able.debug { "$methodName → Acquiring Gatt lock" }
146+
callback.waitForGattReady()
147+
148+
Able.verbose { "$methodName → withContext" }
149+
withContext(dispatcher) {
150+
if (!action.invoke()) {
151+
throw RemoteException("BluetoothGatt.$methodName returned false.")
152+
}
153+
}
154+
155+
Able.verbose { "$methodName ← Waiting for BluetoothGattCallback" }
156+
val response = responseChannel.receive()
157+
158+
Able.info { "$methodName$response" }
159+
return response
160+
}
161+
}

core/src/main/java/Gatt.kt

+2-7
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,6 @@ import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
1212
import android.bluetooth.BluetoothGattDescriptor
1313
import android.bluetooth.BluetoothGattService
1414
import android.bluetooth.BluetoothProfile
15-
import com.juul.able.experimental.messenger.OnCharacteristicChanged
16-
import com.juul.able.experimental.messenger.OnCharacteristicRead
17-
import com.juul.able.experimental.messenger.OnCharacteristicWrite
18-
import com.juul.able.experimental.messenger.OnConnectionStateChange
19-
import com.juul.able.experimental.messenger.OnDescriptorWrite
20-
import com.juul.able.experimental.messenger.OnMtuChanged
2115
import kotlinx.coroutines.experimental.channels.BroadcastChannel
2216
import java.io.Closeable
2317
import java.util.UUID
@@ -96,5 +90,6 @@ interface Gatt : Closeable {
9690
}
9791

9892
suspend fun Gatt.writeCharacteristic(
99-
characteristic: BluetoothGattCharacteristic, value: ByteArray
93+
characteristic: BluetoothGattCharacteristic,
94+
value: ByteArray
10095
): OnCharacteristicWrite = writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT)

0 commit comments

Comments
 (0)