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

Commit

Permalink
Remove Messenger actor and use withContext instead
Browse files Browse the repository at this point in the history
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]: Kotlin/kotlinx.coroutines#87 (comment)
  • Loading branch information
twyatt committed Apr 7, 2020
1 parent 790dbca commit ca93d7b
Show file tree
Hide file tree
Showing 61 changed files with 2,110 additions and 1,470 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
key: grade-${{ hashFiles('**/*.gradle*') }}

- name: Check
run: ./gradlew $GRADLE_ARGS check jacocoTestDebugUnitTestReport
run: ./gradlew $GRADLE_ARGS check jacocoTestReport

- name: Codecov
uses: codecov/codecov-action@v1
Expand Down
84 changes: 29 additions & 55 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ traditionally rely on [`BluetoothGattCallback`] calls with [suspension functions
callback: BluetoothGattCallback
): BluetoothGatt</code></pre></td>
<td><pre><code>suspend fun connectGatt(
context: Context,
autoConnect: Boolean = false
context: Context
): ConnectGattResult</code><sup>1</sup></pre></td>
</tr>
</table>
Expand All @@ -36,8 +35,7 @@ traditionally rely on [`BluetoothGattCallback`] calls with [suspension functions
```kotlin
sealed class ConnectGattResult {
data class Success(val gatt: Gatt) : ConnectGattResult()
data class Canceled(val cause: CancellationException) : ConnectGattResult()
data class Failure(val cause: Throwable) : ConnectGattResult()
data class Failure(val cause: Exception) : ConnectGattResult()
}
```

Expand All @@ -47,10 +45,6 @@ sealed class ConnectGattResult {
<td align="center">Able <code>Gatt</code></td>
</tr>
<tr>
<td><pre><code>fun connect(): Boolean</code></pre></td>
<td><pre><code>suspend fun connect(): Boolean</code><sup>1</sup></pre></td>
</tr>
<tr>
<td><pre><code>fun disconnect(): Boolean</code></pre></td>
<td><pre><code>suspend fun disconnect(): Unit</code><sup>2</sup></pre></td>
</tr>
Expand Down Expand Up @@ -110,18 +104,18 @@ sealed class ConnectGattResult {
</table>

<sup>1</sup> _Suspends until `STATE_CONNECTED` or non-`GATT_SUCCESS` is received._<br/>
<sup>2</sup> _Suspends until `STATE_DISCONNECTED` or non-`GATT_SUCCESS` is received._<br/>
<sup>2</sup> _Suspends until `STATE_DISCONNECTED` or non-`GATT_SUCCESS` is received, then calls `close()` on underlying [`BluetoothGatt`]._<br/>
<sup>3</sup> _Throws [`RemoteException`] if underlying [`BluetoothGatt`] call returns `false`._<br/>
<sup>3</sup> _Throws `GattClosed` if `Gatt` is closed while method is executing._<br/>
<sup>3</sup> _Throws `GattConnectionLost` if `Gatt` is disconnects while method is executing._
<sup>3</sup> _Throws `ConnectionLost` if `Gatt` is closed while method is executing._<br/>

### Details

The primary entry point is the
`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean): ConnectGattResult` extension
function. This extension function acts as a replacement for Android's
`BluetoothDevice.connectGatt(context: Context): ConnectGattResult` extension function. This
extension function acts as a replacement for Android's
[`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean, callback: BluetoothCallback): BluetoothGatt?`]
method (which relies on a [`BluetoothGattCallback`]).
method (which relies on a [`BluetoothGattCallback`]). The `autoConnect` parameter is not
configurable (and is always `false`).

### Prerequisites

Expand All @@ -130,54 +124,34 @@ method (which relies on a [`BluetoothGattCallback`]).
(e.g. [bluetooth permissions]) are satisfied prior to use; failing to do so will result in
[`RemoteException`] for most **Able** methods.

## Structured Concurrency

Kotlin Coroutines `0.26.0` introduced [structured concurrency].
## [Structured Concurrency]

When establishing a connection (e.g. via
`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean): ConnectGattResult` extension
function), if the Coroutine is cancelled then the in-flight connection attempt will be cancelled and
corresponding [`BluetoothGatt`] will be closed:
`BluetoothDevice.connectGatt(context: Context): ConnectGattResult` extension function), if the
Coroutine is cancelled or the connection process fails, then the in-flight connection attempt will
be cancelled and underlying [`BluetoothGatt`] will be closed:

```kotlin
fun connect(context: Context, device: BluetoothDevice) {
val deferred = async {
device.connectGatt(context, autoConnect = false)
val job = launch {
device.connectGatt(context)
}

launch {
delay(1000L) // Assume, for this example, that BLE connection takes more than 1 second.
delay(1_000L) // Assume, for this example, that BLE connection takes more than 1 second.

// Cancels the `async` Coroutine and automatically closes the underlying `BluetoothGatt`.
deferred.cancel()
// Cancels the `launch` Coroutine and automatically closes the underlying `BluetoothGatt`.
job.cancel()
}

val result = deferred.await() // `result` will be `ConnectGattResult.Canceled`.
}
```

Note that in the above example, if the BLE connection takes less than 1 second, then the
**established** connection will **not** be cancelled (and `Gatt` will not be closed), and `result`
will be `ConnectGattResult.Success`.

### `Gatt` Coroutine Scope
**established** connection will **not** be cancelled and `result` will be
`ConnectGattResult.Success`.

**Able**'s `Gatt` provides a [`CoroutineScope`], allowing any Coroutine builders to be scoped to the
`Gatt` instance. For example, you can continually read a characteristic and the Coroutine will
automatically cancel when the `Gatt` is closed (_error handling omitted for simplicity_):

```kotlin
fun continuallyReadCharacteristic(gatt: Gatt, serviceUuid: UUID, characteristicUuid: UUID) {
val characteristic = gatt.getService(serviceUuid)!!.getCharacteristic(characteristicUuid)!!

// This Coroutine will automatically cancel when `gatt.close()` is called.
gatt.launch {
while (isActive) {
println("value = ${gatt.readCharacteristic(characteristic).value}")
}
}
}
```
If `BluetoothDevice.connectGatt(context: Context): ConnectGattResult` returns
`ConnectGattResult.Success` then it will remain connected until `disconnect()` is called.

# Setup

Expand All @@ -195,16 +169,16 @@ dependencies {

**Able** provides a number of packages to help extend it's functionality:

| Package | Functionality |
|-------------------|-----------------------------------------------------------------------------------------------------------------|
| [`processor`] | A `Processor` adds the ability to process (and optionally modify) GATT data<br/>pre-write or post-read. |
| [`throw`] | Adds extension functions that `throw` exceptions on failures for various BLE<br/>operations. |
| [`timber-logger`] | Routes **Able** logging through [Timber](https://github.com/JakeWharton/timber). |
| Package | Functionality |
|-------------------|---------------------------------------------------------------------------------------------------------|
| [`processor`] | A `Processor` adds the ability to process (and optionally modify) GATT data<br/>pre-write or post-read. |
| [`throw`] | Adds extension functions that `throw` exceptions on failures for various BLE<br/>operations. |
| [`timber-logger`] | Routes **Able** logging through [Timber](https://github.com/JakeWharton/timber). |

# License

```
Copyright 2018 JUUL Labs
Copyright 2020 JUUL Labs
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand All @@ -229,8 +203,8 @@ limitations under the License.
[`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean, callback: BluetoothCallback): BluetoothGatt?`]: https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html#connectGatt(android.content.Context,%20boolean,%20android.bluetooth.BluetoothGattCallback)
[`BluetoothAdapter.getDefaultAdapter()`]: https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#getDefaultAdapter()
[bluetooth permissions]: https://developer.android.com/guide/topics/connectivity/bluetooth#Permissions
[structured concurrency]: https://medium.com/@elizarov/structured-concurrency-722d765aa952
[`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-scope/
[Structured Concurrency]: https://medium.com/@elizarov/structured-concurrency-722d765aa952
[`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-scope/
[`processor`]: processor
[`throw`]: throw
[`timber-logger`]: timber-logger
6 changes: 2 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ buildscript {
}

plugins {
id 'org.jetbrains.kotlin.android' version '1.3.60' apply false
id 'org.jetbrains.kotlin.android' version '1.3.70' apply false
id 'org.jmailen.kotlinter' version '2.2.0' apply false
id 'com.android.library' version '3.6.2' apply false
id 'com.vanniktech.maven.publish' version '0.11.1' apply false
Expand All @@ -28,8 +28,7 @@ subprojects {
subprojects {
tasks.withType(Test) {
testLogging {
// For more verbosity add: "standardOut", "standardError"
events "passed", "skipped", "failed"
events "passed", "skipped", "failed", "standardOut", "standardError"
exceptionFormat "full"
showExceptions true
showStackTraces true
Expand All @@ -44,7 +43,6 @@ gradle.taskGraph.whenReady { taskGraph ->
taskGraph.getAllTasks().findAll { task ->
task.name.startsWith('installArchives') || task.name.startsWith('publishArchives')
}.forEach { task ->
logger.error("VERSION_NAME='" + project.findProperty("VERSION_NAME") + "'")
task.doFirst {
if (!project.hasProperty("VERSION_NAME") || project.findProperty("VERSION_NAME").startsWith("unspecified")) {
logger.error("VERSION_NAME=" + project.findProperty("VERSION_NAME"))
Expand Down
7 changes: 3 additions & 4 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
fixes:
- "com/juul/able/experimental::"
- "com/juul/able/experimental/processor::"
- "com/juul/able/experimental/throwable::"
- "com/juul/able/experimental/logger/timber::"
- "com/juul/able/logger/timber::"
- "com/juul/able/processor::"
- "com/juul/able/throwable::"
15 changes: 13 additions & 2 deletions core/build.gradle
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
plugins {
id 'com.android.library'
id 'org.jetbrains.kotlin.android'
id 'org.jmailen.kotlinter'
id 'com.hiya.jacoco-android'
id 'com.vanniktech.maven.publish'
}

apply from: rootProject.file('gradle/jacoco-android.gradle')
jacoco {
toolVersion = "0.8.5"
}

jacocoAndroidUnitTestReport {
csv.enabled false
html.enabled true
xml.enabled true

excludes += ['**/Debug*.*']
}

mavenPublish {
useLegacyMode = true
Expand All @@ -21,7 +32,7 @@ android {

dependencies {
api deps.kotlin.coroutines
api deps.kotlin.junit
testImplementation deps.kotlin.junit
testImplementation deps.mockk
testImplementation deps.equalsverifier
}
4 changes: 2 additions & 2 deletions core/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!--
~ Copyright 2018 JUUL Labs, Inc.
~ Copyright 2020 JUUL Labs, Inc.
-->

<manifest package="com.juul.able.experimental" />
<manifest package="com.juul.able" />
32 changes: 5 additions & 27 deletions core/src/main/java/Able.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
/*
* Copyright 2018 JUUL Labs, Inc.
* Copyright 2020 JUUL Labs, Inc.
*/

package com.juul.able.experimental
package com.juul.able

import android.util.Log

interface Logger {
fun isLoggable(priority: Int): Boolean
fun log(priority: Int, throwable: Throwable? = null, message: String)
}
import com.juul.able.logger.AndroidLogger

object Able {

const val VERBOSE = 2
const val DEBUG = 3
const val INFO = 4
const val WARN = 5
const val ERROR = 6
const val ASSERT = 7

@Volatile
var logger: Logger = AndroidLogger()

inline fun assert(throwable: Throwable? = null, message: () -> String) {
Expand Down Expand Up @@ -48,18 +37,7 @@ object Able {

inline fun log(priority: Int, throwable: Throwable? = null, message: () -> String) {
if (logger.isLoggable(priority)) {
logger.log(priority, throwable, message())
logger.log(priority, throwable, message.invoke())
}
}
}

class AndroidLogger : Logger {

private val tag = "Able"

override fun isLoggable(priority: Int): Boolean = Log.isLoggable(tag, priority)

override fun log(priority: Int, throwable: Throwable?, message: String) {
Log.println(priority, tag, message)
}
}
67 changes: 0 additions & 67 deletions core/src/main/java/ConnectionStateMonitor.kt

This file was deleted.

Loading

0 comments on commit ca93d7b

Please sign in to comment.