From 36f763a8136a2769f03b0ebbbf27c6737337879b Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Mon, 1 Oct 2018 11:04:59 -0700 Subject: [PATCH] Remove Messenger actor and use withContext instead Inspired by [comment] by elizarov (on Jun 15) in Kotlin/kotlinx.coroutines#87: > 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]: https://github.com/Kotlin/kotlinx.coroutines/issues/87#issuecomment-397554104 --- core/src/main/java/ConnectionStateMonitor.kt | 1 - core/src/main/java/CoroutinesDevice.kt | 6 +- core/src/main/java/CoroutinesGatt.kt | 207 ++++++------------ core/src/main/java/Gatt.kt | 9 +- .../main/java/{messenger => }/GattCallback.kt | 72 +++--- .../{messenger/Messages.kt => Responses.kt} | 91 ++++---- core/src/main/java/android/BluetoothDevice.kt | 2 +- core/src/main/java/messenger/Messenger.kt | 74 ------- core/src/test/java/CoroutinesGattTest.kt | 9 +- .../{MessagesTest.kt => ResponsesTest.kt} | 5 +- device/src/main/java/CoroutinesGattDevice.kt | 14 +- .../src/main/java/android/BluetoothDevice.kt | 12 +- processor/src/main/java/GattProcessor.kt | 6 +- retry/src/main/java/Retry.kt | 6 +- .../src/main/java/android/BluetoothDevice.kt | 4 +- 15 files changed, 188 insertions(+), 330 deletions(-) rename core/src/main/java/{messenger => }/GattCallback.kt (70%) rename core/src/main/java/{messenger/Messages.kt => Responses.kt} (60%) delete mode 100644 core/src/main/java/messenger/Messenger.kt rename core/src/test/java/{MessagesTest.kt => ResponsesTest.kt} (90%) diff --git a/core/src/main/java/ConnectionStateMonitor.kt b/core/src/main/java/ConnectionStateMonitor.kt index 85adc32..4274207 100644 --- a/core/src/main/java/ConnectionStateMonitor.kt +++ b/core/src/main/java/ConnectionStateMonitor.kt @@ -8,7 +8,6 @@ package com.juul.able.experimental import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothProfile -import com.juul.able.experimental.messenger.OnConnectionStateChange import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.sync.Mutex diff --git a/core/src/main/java/CoroutinesDevice.kt b/core/src/main/java/CoroutinesDevice.kt index 718114d..d5e97e7 100644 --- a/core/src/main/java/CoroutinesDevice.kt +++ b/core/src/main/java/CoroutinesDevice.kt @@ -12,9 +12,6 @@ import android.os.RemoteException import com.juul.able.experimental.ConnectGattResult.Canceled import com.juul.able.experimental.ConnectGattResult.Failure import com.juul.able.experimental.ConnectGattResult.Success -import com.juul.able.experimental.messenger.GattCallback -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.Messenger import kotlinx.coroutines.CancellationException class CoroutinesDevice( @@ -31,8 +28,7 @@ class CoroutinesDevice( private fun requestConnectGatt(context: Context, autoConnect: Boolean): CoroutinesGatt? { val callback = GattCallback(callbackConfig) val bluetoothGatt = device.connectGatt(context, autoConnect, callback) ?: return null - val messenger = Messenger(bluetoothGatt, callback) - return CoroutinesGatt(bluetoothGatt, messenger) + return CoroutinesGatt(bluetoothGatt, callback) } /** diff --git a/core/src/main/java/CoroutinesGatt.kt b/core/src/main/java/CoroutinesGatt.kt index 896a7e2..d0fa5c1 100644 --- a/core/src/main/java/CoroutinesGatt.kt +++ b/core/src/main/java/CoroutinesGatt.kt @@ -14,25 +14,16 @@ import android.bluetooth.BluetoothProfile.STATE_CONNECTED import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED import android.bluetooth.BluetoothProfile.STATE_DISCONNECTING import android.os.RemoteException -import com.juul.able.experimental.messenger.Message.DiscoverServices -import com.juul.able.experimental.messenger.Message.ReadCharacteristic -import com.juul.able.experimental.messenger.Message.RequestMtu -import com.juul.able.experimental.messenger.Message.WriteCharacteristic -import com.juul.able.experimental.messenger.Message.WriteDescriptor -import com.juul.able.experimental.messenger.Messenger -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged -import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExecutorCoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.ClosedReceiveChannelException import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.channels.filter +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.selects.select +import kotlinx.coroutines.withContext import java.util.UUID import kotlin.coroutines.CoroutineContext @@ -41,7 +32,8 @@ class GattConnectionLost : Exception() class CoroutinesGatt( private val bluetoothGatt: BluetoothGatt, - private val messenger: Messenger + private val callback: GattCallback, + private val dispatcher: CoroutineDispatcher = newSingleThreadContext("Gatt") ) : Gatt { private val job = Job() @@ -51,10 +43,10 @@ class CoroutinesGatt( private val connectionStateMonitor by lazy { ConnectionStateMonitor(this) } override val onConnectionStateChange: BroadcastChannel - get() = messenger.callback.onConnectionStateChange + get() = callback.onConnectionStateChange override val onCharacteristicChanged: BroadcastChannel - get() = messenger.callback.onCharacteristicChanged + get() = callback.onCharacteristicChanged override fun requestConnect(): Boolean = bluetoothGatt.connect() override fun requestDisconnect(): Unit = bluetoothGatt.disconnect() @@ -77,7 +69,18 @@ class CoroutinesGatt( Able.verbose { "close → Begin" } job.cancel() connectionStateMonitor.close() - messenger.close() + callback.close() + + if (dispatcher is ExecutorCoroutineDispatcher) { + /** + * Explicitly close context (this is needed until #261 is fixed). + * + * [Kotlin Coroutines Issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) + * [Coroutines actor test Gist](https://gist.github.com/twyatt/c51f81d763a6ee39657233fa725f5435) + */ + dispatcher.close() + } + bluetoothGatt.close() Able.verbose { "close → End" } } @@ -89,62 +92,22 @@ class CoroutinesGatt( * @throws [RemoteException] if underlying [BluetoothGatt.discoverServices] returns `false`. * @throws [GattClosed] if [Gatt] is closed while method is executing. */ - override suspend fun discoverServices(): GattStatus { - Able.debug { "discoverServices → send(DiscoverServices)" } - - val response = CompletableDeferred() - messenger.send(DiscoverServices(response)) - - val call = "BluetoothGatt.discoverServices()" - Able.verbose { "discoverServices → Waiting for $call" } - if (!response.await()) { - throw RemoteException("$call returned false.") + override suspend fun discoverServices(): GattStatus = + performBluetoothAction("discoverServices", callback.onServicesDiscovered) { + bluetoothGatt.discoverServices() } - Able.verbose { "discoverServices → Waiting for BluetoothGattCallback" } - return try { - messenger.callback.onServicesDiscovered.receiveRequiringConnection().also { status -> - Able.info { "discoverServices, status=${status.asGattStatusString()}" } - } - } catch (e: ClosedReceiveChannelException) { - throw GattClosed("Gatt closed during discoverServices", e) - } - } - /** * @throws [RemoteException] if underlying [BluetoothGatt.readCharacteristic] returns `false`. * @throws [GattClosed] if [Gatt] is closed while method is executing. */ override suspend fun readCharacteristic( characteristic: BluetoothGattCharacteristic - ): OnCharacteristicRead { - val uuid = characteristic.uuid - Able.debug { "readCharacteristic → send(ReadCharacteristic[uuid=$uuid])" } - - val response = CompletableDeferred() - messenger.send(ReadCharacteristic(characteristic, response)) - - val call = "BluetoothGatt.readCharacteristic(BluetoothGattCharacteristic[uuid=$uuid])" - Able.verbose { "readCharacteristic → Waiting for $call" } - if (!response.await()) { - throw RemoteException("Failed to read characteristic with UUID $uuid.") + ): OnCharacteristicRead = + performBluetoothAction("readCharacteristic", callback.onCharacteristicRead) { + bluetoothGatt.readCharacteristic(characteristic) } - Able.verbose { "readCharacteristic → Waiting for BluetoothGattCallback" } - return try { - messenger.callback.onCharacteristicRead.receiveRequiringConnection() - .also { (_, value, status) -> - Able.info { - val bytesString = value.size.bytesString - val statusString = status.asGattStatusString() - "← readCharacteristic $uuid ($bytesString), status=$statusString" - } - } - } catch (e: ClosedReceiveChannelException) { - throw GattClosed("Gatt closed during readCharacteristic[uuid=$uuid]", e) - } - } - /** * @param value applied to [characteristic] when characteristic is written. * @param writeType applied to [characteristic] when characteristic is written. @@ -155,103 +118,71 @@ class CoroutinesGatt( characteristic: BluetoothGattCharacteristic, value: ByteArray, writeType: WriteType - ): OnCharacteristicWrite { - val uuid = characteristic.uuid - Able.debug { "writeCharacteristic → send(WriteCharacteristic[uuid=$uuid])" } - - val response = CompletableDeferred() - messenger.send(WriteCharacteristic(characteristic, value, writeType, response)) - - val call = "BluetoothGatt.writeCharacteristic(BluetoothGattCharacteristic[uuid=$uuid])" - Able.verbose { "writeCharacteristic → Waiting for $call" } - if (!response.await()) { - throw RemoteException("$call returned false.") + ): OnCharacteristicWrite = + performBluetoothAction("writeCharacteristic", callback.onCharacteristicWrite) { + characteristic.value = value + characteristic.writeType = writeType + bluetoothGatt.writeCharacteristic(characteristic) } - Able.verbose { "writeCharacteristic → Waiting for BluetoothGattCallback" } - return try { - messenger.callback.onCharacteristicWrite.receiveRequiringConnection() - .also { (_, status) -> - Able.info { - val bytesString = value.size.bytesString - val typeString = writeType.asWriteTypeString() - val statusString = status.asGattStatusString() - "→ writeCharacteristic $uuid ($bytesString), type=$typeString, status=$statusString" - } - } - } catch (e: ClosedReceiveChannelException) { - throw GattClosed("Gatt closed during writeCharacteristic[uuid=$uuid]", e) - } - } - /** * @param value applied to [descriptor] when descriptor is written. * @throws [RemoteException] if underlying [BluetoothGatt.writeDescriptor] returns `false`. * @throws [GattClosed] if [Gatt] is closed while method is executing. */ override suspend fun writeDescriptor( - descriptor: BluetoothGattDescriptor, value: ByteArray - ): OnDescriptorWrite { - val uuid = descriptor.uuid - Able.debug { "writeDescriptor → send(WriteDescriptor[uuid=$uuid])" } - - val response = CompletableDeferred() - messenger.send(WriteDescriptor(descriptor, value, response)) - - val call = "BluetoothGatt.writeDescriptor(BluetoothGattDescriptor[uuid=$uuid])" - Able.verbose { "writeDescriptor → Waiting for $call" } - if (!response.await()) { - throw RemoteException("$call returned false.") - } - - Able.verbose { "writeDescriptor → Waiting for BluetoothGattCallback" } - return try { - messenger.callback.onDescriptorWrite.receiveRequiringConnection().also { (_, status) -> - Able.info { - val bytesString = value.size.bytesString - val statusString = status.asGattStatusString() - "→ writeDescriptor $uuid ($bytesString), status=$statusString" - } - } - } catch (e: ClosedReceiveChannelException) { - throw GattClosed("Gatt closed during writeDescriptor[uuid=$uuid]", e) + descriptor: BluetoothGattDescriptor, + value: ByteArray + ): OnDescriptorWrite = + performBluetoothAction("writeDescriptor", callback.onDescriptorWrite) { + descriptor.value = value + bluetoothGatt.writeDescriptor(descriptor) } - } /** * @throws [RemoteException] if underlying [BluetoothGatt.requestMtu] returns `false`. * @throws [GattClosed] if [Gatt] is closed while method is executing. */ - override suspend fun requestMtu(mtu: Int): OnMtuChanged { - Able.debug { "requestMtu → send(RequestMtu[mtu=$mtu])" } - - val response = CompletableDeferred() - messenger.send(RequestMtu(mtu, response)) - - val call = "BluetoothGatt.requestMtu($mtu)" - Able.verbose { "requestMtu → Waiting for $call" } - if (!response.await()) { - throw RemoteException("$call returned false.") - } - - Able.verbose { "requestMtu → Waiting for BluetoothGattCallback" } - return try { - messenger.callback.onMtuChanged.receiveRequiringConnection().also { (mtu, status) -> - Able.info { "requestMtu $mtu, status=${status.asGattStatusString()}" } - } - } catch (e: ClosedReceiveChannelException) { - throw GattClosed("Gatt closed during requestMtu[mtu=$mtu]", e) + override suspend fun requestMtu(mtu: Int): OnMtuChanged = + performBluetoothAction("requestMtu", callback.onMtuChanged) { + bluetoothGatt.requestMtu(mtu) } - } override fun setCharacteristicNotification( characteristic: BluetoothGattCharacteristic, enable: Boolean ): Boolean { - Able.info { "setCharacteristicNotification ${characteristic.uuid} enable=$enable" } + Able.info { "setCharacteristicNotification → uuid=${characteristic.uuid}, enable=$enable" } return bluetoothGatt.setCharacteristicNotification(characteristic, enable) } + private suspend fun performBluetoothAction( + methodName: String, + responseChannel: ReceiveChannel, + action: () -> Boolean + ): T { + Able.debug { "$methodName → Acquiring Gatt lock" } + callback.waitForGattReady() + + Able.verbose { "$methodName → withContext" } + withContext(dispatcher) { + if (!action.invoke()) { + callback.notifyGattReady() + throw RemoteException("BluetoothGatt.$methodName returned false.") + } + } + + Able.verbose { "$methodName ← Waiting for BluetoothGattCallback" } + val response = try { + responseChannel.receiveRequiringConnection() + } catch (e: ClosedReceiveChannelException) { + throw GattClosed("Gatt closed during $methodName", e) + } + + Able.info { "$methodName ← $response" } + return response + } + private suspend fun ReceiveChannel.receiveRequiringConnection(): T = select { onReceive { it } @@ -263,5 +194,3 @@ class CoroutinesGatt( .onReceive { throw GattConnectionLost() } } } - -private val Int.bytesString get() = if (this == 1) "$this byte" else "$this bytes" diff --git a/core/src/main/java/Gatt.kt b/core/src/main/java/Gatt.kt index 1714713..7ad37ec 100644 --- a/core/src/main/java/Gatt.kt +++ b/core/src/main/java/Gatt.kt @@ -12,12 +12,6 @@ import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.bluetooth.BluetoothProfile -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.BroadcastChannel import java.io.Closeable @@ -97,5 +91,6 @@ interface Gatt : Closeable, CoroutineScope { } suspend fun Gatt.writeCharacteristic( - characteristic: BluetoothGattCharacteristic, value: ByteArray + characteristic: BluetoothGattCharacteristic, + value: ByteArray ): OnCharacteristicWrite = writeCharacteristic(characteristic, value, WRITE_TYPE_DEFAULT) diff --git a/core/src/main/java/messenger/GattCallback.kt b/core/src/main/java/GattCallback.kt similarity index 70% rename from core/src/main/java/messenger/GattCallback.kt rename to core/src/main/java/GattCallback.kt index 68b0adc..22e565e 100644 --- a/core/src/main/java/messenger/GattCallback.kt +++ b/core/src/main/java/GattCallback.kt @@ -2,17 +2,12 @@ * Copyright 2018 JUUL Labs, Inc. */ -package com.juul.able.experimental.messenger +package com.juul.able.experimental import android.bluetooth.BluetoothGatt import android.bluetooth.BluetoothGattCallback import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor -import com.juul.able.experimental.Able -import com.juul.able.experimental.GattState -import com.juul.able.experimental.GattStatus -import com.juul.able.experimental.asGattConnectionStatusString -import com.juul.able.experimental.asGattStateString import kotlinx.coroutines.channels.BroadcastChannel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.Mutex @@ -39,7 +34,8 @@ data class GattCallbackConfig( ) } -internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback() { +@Suppress("TooManyFunctions") // We're at the mercy of Android's BluetoothGattCallback. +class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback() { internal val onConnectionStateChange = BroadcastChannel(Channel.CONFLATED) @@ -79,7 +75,7 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( onDescriptorWrite.close() onReliableWriteCompleted.close() - Able.verbose { "close → End" } + Able.verbose { "close ← End" } } override fun onConnectionStateChange( @@ -90,18 +86,18 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( Able.debug { val statusString = status.asGattConnectionStatusString() val stateString = newState.asGattStateString() - "onConnectionStateChange → status = $statusString, newState = $stateString" + "onConnectionStateChange ← status=$statusString, newState=$stateString" } if (!onConnectionStateChange.offer(OnConnectionStateChange(status, newState))) { - Able.warn { "onConnectionStateChange → dropped" } + Able.warn { "onConnectionStateChange ↓ dropped" } } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: GattStatus) { - Able.verbose { "onServicesDiscovered → status = $status" } + Able.verbose { "onServicesDiscovered ← status=${status.asGattStatusString()}" } if (!onServicesDiscovered.offer(status)) { - Able.warn { "onServicesDiscovered → dropped" } + Able.warn { "onServicesDiscovered ↓ dropped" } } notifyGattReady() } @@ -111,10 +107,13 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( characteristic: BluetoothGattCharacteristic, status: GattStatus ) { - Able.verbose { "onCharacteristicRead → uuid = ${characteristic.uuid}" } - val event = OnCharacteristicRead(characteristic, characteristic.value, status) + val value = characteristic.value + Able.verbose { + "onCharacteristicRead ← uuid=${characteristic.uuid}, value.size=${value.size}" + } + val event = OnCharacteristicRead(characteristic, value, status) if (!onCharacteristicRead.offer(event)) { - Able.warn { "onCharacteristicRead → dropped" } + Able.warn { "onCharacteristicRead ↓ dropped" } } notifyGattReady() } @@ -124,9 +123,12 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( characteristic: BluetoothGattCharacteristic, status: GattStatus ) { - Able.verbose { "onCharacteristicWrite → uuid = ${characteristic.uuid}" } + Able.verbose { + val uuid = characteristic.uuid + "onCharacteristicWrite ← uuid=$uuid, status=${status.asGattStatusString()}" + } if (!onCharacteristicWrite.offer(OnCharacteristicWrite(characteristic, status))) { - Able.warn { "onCharacteristicWrite → dropped" } + Able.warn { "onCharacteristicWrite ↓ dropped" } } notifyGattReady() } @@ -135,10 +137,12 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic ) { - Able.verbose { "onCharacteristicChanged → uuid = ${characteristic.uuid}" } - val event = OnCharacteristicChanged(characteristic, characteristic.value) - if (!onCharacteristicChanged.offer(event)) { - Able.warn { "OnCharacteristicChanged → dropped" } + val value = characteristic.value + Able.verbose { + "onCharacteristicChanged ← uuid=${characteristic.uuid}, value.size=${value.size}" + } + if (!onCharacteristicChanged.offer(OnCharacteristicChanged(characteristic, value))) { + Able.warn { "OnCharacteristicChanged ↓ dropped" } } // We don't call `notifyGattReady` because `onCharacteristicChanged` is called whenever a @@ -151,9 +155,15 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( descriptor: BluetoothGattDescriptor, status: GattStatus ) { - Able.verbose { "onDescriptorRead → uuid = ${descriptor.uuid}" } - if (!onDescriptorRead.offer(OnDescriptorRead(descriptor, descriptor.value, status))) { - Able.warn { "onDescriptorRead → dropped" } + val value = descriptor.value + Able.verbose { + val uuid = descriptor.uuid + val size = value.size + val gattStatus = status.asGattStatusString() + "onDescriptorRead ← uuid=$uuid, value.size=$size, status=$gattStatus" + } + if (!onDescriptorRead.offer(OnDescriptorRead(descriptor, value, status))) { + Able.warn { "onDescriptorRead ↓ dropped" } } notifyGattReady() } @@ -163,25 +173,27 @@ internal class GattCallback(config: GattCallbackConfig) : BluetoothGattCallback( descriptor: BluetoothGattDescriptor, status: GattStatus ) { - Able.verbose { "onDescriptorWrite → uuid = ${descriptor.uuid}" } + Able.verbose { + "onDescriptorWrite ← uuid=${descriptor.uuid}, status=${status.asGattStatusString()}" + } if (!onDescriptorWrite.offer(OnDescriptorWrite(descriptor, status))) { - Able.warn { "onDescriptorWrite → dropped" } + Able.warn { "onDescriptorWrite ↓ dropped" } } notifyGattReady() } override fun onReliableWriteCompleted(gatt: BluetoothGatt, status: Int) { - Able.verbose { "onReliableWriteCompleted → status = $status" } + Able.verbose { "onReliableWriteCompleted ← status=${status.asGattStatusString()}" } if (!onReliableWriteCompleted.offer(OnReliableWriteCompleted(status))) { - Able.warn { "onReliableWriteCompleted → dropped" } + Able.warn { "onReliableWriteCompleted ↓ dropped" } } notifyGattReady() } override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { - Able.verbose { "onMtuChanged → status = $status" } + Able.verbose { "onMtuChanged ← mtu=$mtu, status=${status.asGattStatusString()}" } if (!onMtuChanged.offer(OnMtuChanged(mtu, status))) { - Able.warn { "onMtuChanged → dropped" } + Able.warn { "onMtuChanged ↓ dropped" } } notifyGattReady() } diff --git a/core/src/main/java/messenger/Messages.kt b/core/src/main/java/Responses.kt similarity index 60% rename from core/src/main/java/messenger/Messages.kt rename to core/src/main/java/Responses.kt index 894b02f..6fbeffc 100644 --- a/core/src/main/java/messenger/Messages.kt +++ b/core/src/main/java/Responses.kt @@ -2,51 +2,22 @@ * Copyright 2018 JUUL Labs, Inc. */ -package com.juul.able.experimental.messenger +package com.juul.able.experimental import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor -import com.juul.able.experimental.GattState -import com.juul.able.experimental.GattStatus -import kotlinx.coroutines.CompletableDeferred import java.util.Arrays -internal sealed class Message { - - abstract val response: CompletableDeferred - - internal data class DiscoverServices( - override val response: CompletableDeferred - ) : Message() - - internal data class ReadCharacteristic( - val characteristic: BluetoothGattCharacteristic, - override val response: CompletableDeferred - ) : Message() - - internal data class WriteCharacteristic( - val characteristic: BluetoothGattCharacteristic, - val value: ByteArray, - val writeType: Int, - override val response: CompletableDeferred - ) : Message() - - internal data class RequestMtu( - val mtu: Int, - override val response: CompletableDeferred - ) : Message() - - internal data class WriteDescriptor( - val descriptor: BluetoothGattDescriptor, - val value: ByteArray, - override val response: CompletableDeferred - ) : Message() -} - data class OnConnectionStateChange( val status: GattStatus, val newState: GattState -) +) { + override fun toString(): String { + val connectionStatus = status.asGattConnectionStatusString() + val gattState = newState.asGattStateString() + return "OnConnectionStateChange(status=$connectionStatus, newState=$gattState}" + } +} data class OnCharacteristicRead( val characteristic: BluetoothGattCharacteristic, @@ -72,12 +43,25 @@ data class OnCharacteristicRead( result = 31 * result + status return result } + + override fun toString(): String { + val uuid = characteristic.uuid + val size = value.size + val gattStatus = status.asGattStatusString() + return "OnCharacteristicRead(uuid=$uuid, value=$value, value.size=$size, status=$gattStatus)" + } } data class OnCharacteristicWrite( val characteristic: BluetoothGattCharacteristic, val status: GattStatus -) +) { + override fun toString(): String { + val uuid = characteristic.uuid + val gattStatus = status.asGattStatusString() + return "OnCharacteristicWrite(uuid=$uuid, status=$gattStatus)" + } +} data class OnCharacteristicChanged( val characteristic: BluetoothGattCharacteristic, @@ -99,6 +83,12 @@ data class OnCharacteristicChanged( result = 31 * result + Arrays.hashCode(value) return result } + + override fun toString(): String { + val uuid = characteristic.uuid + val size = value.size + return "OnCharacteristicChanged(uuid=$uuid, value=$value, value.size=$size)" + } } data class OnDescriptorRead( @@ -125,13 +115,32 @@ data class OnDescriptorRead( result = 31 * result + status return result } + + override fun toString(): String { + val uuid = descriptor.uuid + val size = value.size + val gattStatus = status.asGattStatusString() + return "OnDescriptorRead(uuid=$uuid, value=$value, value.size=$size, status=$gattStatus)" + } } data class OnDescriptorWrite( val descriptor: BluetoothGattDescriptor, val status: GattStatus -) +) { + override fun toString(): String { + val uuid = descriptor.uuid + val gattStatus = status.asGattStatusString() + return "OnDescriptorWrite(uuid=$uuid, status=$gattStatus)" + } +} -data class OnReliableWriteCompleted(val status: GattStatus) +data class OnReliableWriteCompleted(val status: GattStatus) { + override fun toString(): String = + "OnReliableWriteCompleted(status=${status.asGattStatusString()})" +} -data class OnMtuChanged(val mtu: Int, val status: GattStatus) +data class OnMtuChanged(val mtu: Int, val status: GattStatus) { + override fun toString(): String = + "OnMtuChanged(mtu=$mtu, status=${status.asGattStatusString()})" +} diff --git a/core/src/main/java/android/BluetoothDevice.kt b/core/src/main/java/android/BluetoothDevice.kt index 744d31e..41369e7 100644 --- a/core/src/main/java/android/BluetoothDevice.kt +++ b/core/src/main/java/android/BluetoothDevice.kt @@ -11,8 +11,8 @@ import android.content.Context import com.juul.able.experimental.ConnectGattResult import com.juul.able.experimental.CoroutinesDevice import com.juul.able.experimental.Gatt +import com.juul.able.experimental.GattCallbackConfig import com.juul.able.experimental.connectGattOrNull -import com.juul.able.experimental.messenger.GattCallbackConfig fun BluetoothDevice.asCoroutinesDevice( callbackConfig: GattCallbackConfig = GattCallbackConfig() diff --git a/core/src/main/java/messenger/Messenger.kt b/core/src/main/java/messenger/Messenger.kt deleted file mode 100644 index b5b890d..0000000 --- a/core/src/main/java/messenger/Messenger.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.messenger - -import android.bluetooth.BluetoothGatt -import com.juul.able.experimental.Able -import com.juul.able.experimental.messenger.Message.DiscoverServices -import com.juul.able.experimental.messenger.Message.ReadCharacteristic -import com.juul.able.experimental.messenger.Message.RequestMtu -import com.juul.able.experimental.messenger.Message.WriteCharacteristic -import com.juul.able.experimental.messenger.Message.WriteDescriptor -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.newSingleThreadContext - -class Messenger internal constructor( - private val bluetoothGatt: BluetoothGatt, - internal val callback: GattCallback -) { - - internal suspend fun send(message: Message) = actor.send(message) - - private val context = newSingleThreadContext("Gatt") - private val actor = GlobalScope.actor(context) { - Able.verbose { "Begin" } - consumeEach { message -> - Able.debug { "Waiting for Gatt" } - callback.waitForGattReady() - - Able.debug { "Processing ${message.javaClass.simpleName}" } - val result: Boolean = when (message) { - is DiscoverServices -> bluetoothGatt.discoverServices() - is ReadCharacteristic -> bluetoothGatt.readCharacteristic(message.characteristic) - is RequestMtu -> bluetoothGatt.requestMtu(message.mtu) - is WriteCharacteristic -> { - message.characteristic.value = message.value - message.characteristic.writeType = message.writeType - bluetoothGatt.writeCharacteristic(message.characteristic) - } - is WriteDescriptor -> { - message.descriptor.value = message.value - bluetoothGatt.writeDescriptor(message.descriptor) - } - } - - Able.debug { "Processed ${message.javaClass.simpleName}, result=$result" } - message.response.complete(result) - - if (!result) { - callback.notifyGattReady() - } - } - Able.verbose { "End" } - } - - fun close() { - Able.verbose { "close → Begin" } - callback.close() - actor.close() - closeContext() - Able.verbose { "close → End" } - } - - /** - * Explicitly close context (this is only needed until #261 is fixed). - * - * [Kotlin Coroutines Issue #261](https://github.com/Kotlin/kotlinx.coroutines/issues/261) - * [Coroutines actor test Gist](https://gist.github.com/twyatt/c51f81d763a6ee39657233fa725f5435) - */ - private fun closeContext(): Unit = context.close() -} diff --git a/core/src/test/java/CoroutinesGattTest.kt b/core/src/test/java/CoroutinesGattTest.kt index b444c89..eb5c4b1 100644 --- a/core/src/test/java/CoroutinesGattTest.kt +++ b/core/src/test/java/CoroutinesGattTest.kt @@ -9,9 +9,6 @@ import android.bluetooth.BluetoothGatt.GATT_SUCCESS import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothProfile.STATE_CONNECTED import android.os.RemoteException -import com.juul.able.experimental.messenger.GattCallback -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.Messenger import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.channels.consumeEach @@ -48,8 +45,7 @@ class CoroutinesGattTest { val callback = GattCallback(GattCallbackConfig(onCharacteristicChangedCapacity)).apply { onConnectionStateChange(bluetoothGatt, GATT_SUCCESS, STATE_CONNECTED) } - val messenger = Messenger(bluetoothGatt, callback) - val gatt = CoroutinesGatt(bluetoothGatt, messenger) + val gatt = CoroutinesGatt(bluetoothGatt, callback) val binderThreads = FakeBinderThreadHandler(numberOfFakeBinderThreads) for (i in 0..numberOfFakeCharacteristicNotifications) { @@ -86,8 +82,7 @@ class CoroutinesGattTest { val callback = GattCallback(GattCallbackConfig()).apply { onConnectionStateChange(bluetoothGatt, GATT_SUCCESS, STATE_CONNECTED) } - val messenger = Messenger(bluetoothGatt, callback) - val gatt = CoroutinesGatt(bluetoothGatt, messenger) + val gatt = CoroutinesGatt(bluetoothGatt, callback) assertFailsWith(RemoteException::class, "First invocation") { runBlocking { diff --git a/core/src/test/java/MessagesTest.kt b/core/src/test/java/ResponsesTest.kt similarity index 90% rename from core/src/test/java/MessagesTest.kt rename to core/src/test/java/ResponsesTest.kt index c70e546..8894695 100644 --- a/core/src/test/java/MessagesTest.kt +++ b/core/src/test/java/ResponsesTest.kt @@ -6,9 +6,6 @@ package com.juul.able.experimental import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnDescriptorRead import io.mockk.every import io.mockk.mockk import nl.jqno.equalsverifier.EqualsVerifier @@ -20,7 +17,7 @@ private val blackDescriptor = mockDescriptor("de923c26-b18a-474a-84e0-7837300fc6 private val redCharacteristic = mockCharacteristic("63057836-0b22-4341-969a-8fee3a8be2b3") private val blackCharacteristic = mockCharacteristic("2a5346f9-1aec-4752-acec-5d269aa96e7d") -class MessagesTest { +class ResponsesTest { @Test fun onCharacteristicReadEquals() { diff --git a/device/src/main/java/CoroutinesGattDevice.kt b/device/src/main/java/CoroutinesGattDevice.kt index b41098f..6743ea8 100644 --- a/device/src/main/java/CoroutinesGattDevice.kt +++ b/device/src/main/java/CoroutinesGattDevice.kt @@ -14,17 +14,17 @@ import android.content.Context import com.juul.able.experimental.Able import com.juul.able.experimental.ConnectGattResult import com.juul.able.experimental.Gatt +import com.juul.able.experimental.GattCallbackConfig import com.juul.able.experimental.GattState import com.juul.able.experimental.GattStatus +import com.juul.able.experimental.OnCharacteristicChanged +import com.juul.able.experimental.OnCharacteristicRead +import com.juul.able.experimental.OnCharacteristicWrite +import com.juul.able.experimental.OnConnectionStateChange +import com.juul.able.experimental.OnDescriptorWrite +import com.juul.able.experimental.OnMtuChanged import com.juul.able.experimental.WriteType import com.juul.able.experimental.android.connectGatt -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job diff --git a/device/src/main/java/android/BluetoothDevice.kt b/device/src/main/java/android/BluetoothDevice.kt index e4c95b1..fcb596d 100644 --- a/device/src/main/java/android/BluetoothDevice.kt +++ b/device/src/main/java/android/BluetoothDevice.kt @@ -13,15 +13,15 @@ import android.bluetooth.BluetoothGattDescriptor import android.bluetooth.BluetoothGattService import android.content.Context import com.juul.able.experimental.ConnectGattResult +import com.juul.able.experimental.GattCallbackConfig import com.juul.able.experimental.GattStatus +import com.juul.able.experimental.OnCharacteristicChanged +import com.juul.able.experimental.OnCharacteristicRead +import com.juul.able.experimental.OnConnectionStateChange +import com.juul.able.experimental.OnDescriptorWrite +import com.juul.able.experimental.OnMtuChanged import com.juul.able.experimental.WriteType import com.juul.able.experimental.device.CoroutinesGattDevices -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged import kotlinx.coroutines.channels.BroadcastChannel import java.util.UUID diff --git a/processor/src/main/java/GattProcessor.kt b/processor/src/main/java/GattProcessor.kt index f900d9f..d11f52a 100644 --- a/processor/src/main/java/GattProcessor.kt +++ b/processor/src/main/java/GattProcessor.kt @@ -7,10 +7,10 @@ package com.juul.able.experimental.processor import android.bluetooth.BluetoothGattCharacteristic import android.bluetooth.BluetoothGattDescriptor import com.juul.able.experimental.Gatt +import com.juul.able.experimental.OnCharacteristicRead +import com.juul.able.experimental.OnCharacteristicWrite +import com.juul.able.experimental.OnDescriptorWrite import com.juul.able.experimental.WriteType -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnDescriptorWrite fun Gatt.withProcessors(vararg processors: Processor) = GattProcessor(this, processors) diff --git a/retry/src/main/java/Retry.kt b/retry/src/main/java/Retry.kt index 8fa6525..6c8966e 100644 --- a/retry/src/main/java/Retry.kt +++ b/retry/src/main/java/Retry.kt @@ -13,10 +13,10 @@ import android.bluetooth.BluetoothProfile.STATE_CONNECTED import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED import com.juul.able.experimental.Able import com.juul.able.experimental.Gatt +import com.juul.able.experimental.OnCharacteristicRead +import com.juul.able.experimental.OnCharacteristicWrite +import com.juul.able.experimental.OnDescriptorWrite import com.juul.able.experimental.WriteType -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnDescriptorWrite import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.channels.consumeEach import kotlinx.coroutines.sync.Mutex diff --git a/throw/src/main/java/android/BluetoothDevice.kt b/throw/src/main/java/android/BluetoothDevice.kt index 2bfdb4d..93d36cb 100644 --- a/throw/src/main/java/android/BluetoothDevice.kt +++ b/throw/src/main/java/android/BluetoothDevice.kt @@ -8,11 +8,11 @@ import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt.GATT_SUCCESS import android.content.Context import com.juul.able.experimental.Gatt +import com.juul.able.experimental.GattCallbackConfig import com.juul.able.experimental.android.asCoroutinesDevice -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.throwable.connectGattOrThrow import com.juul.able.experimental.throwable.ConnectionCanceledException import com.juul.able.experimental.throwable.ConnectionFailedException +import com.juul.able.experimental.throwable.connectGattOrThrow import kotlinx.coroutines.CancellationException /**