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

Remove Messenger actor and use withContext instead #31

Merged
merged 19 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -197,16 +171,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 @@ -231,8 +205,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 "started", "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" />
39 changes: 12 additions & 27 deletions core/src/main/java/Able.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
/*
* 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.ASSERT
import com.juul.able.logger.AndroidLogger
import com.juul.able.logger.DEBUG
import com.juul.able.logger.ERROR
import com.juul.able.logger.INFO
import com.juul.able.logger.Logger
import com.juul.able.logger.VERBOSE
import com.juul.able.logger.WARN

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 +44,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