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

Commit 790dc5f

Browse files
committed
Bump kotlinx.coroutines version to 0.26.1 (#11)
Bumped Kotlin version to `1.2.61`, as the `kotlinx.coroutines` [GitHub page] states: > This is a companion version for Kotlin 1.2.61 release. [GitHub page]: https://github.com/Kotlin/kotlinx.coroutines
1 parent 2823089 commit 790dc5f

File tree

4 files changed

+140
-5
lines changed

4 files changed

+140
-5
lines changed

README.md

Lines changed: 133 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ sealed class ConnectGattResult {
107107

108108
<sup>1</sup> _Suspends until `STATE_CONNECTED` or non-`GATT_SUCCESS` is received._<br/>
109109
<sup>2</sup> _Suspends until `STATE_DISCONNECTED` or non-`GATT_SUCCESS` is received._<br/>
110-
<sup>3</sup> _Throws `RemoteException` if underlying `BluetoothGatt` call returns `false`._
110+
<sup>3</sup> _Throws [`RemoteException`] if underlying [`BluetoothGatt`] call returns `false`._
111111

112112
### Details
113113

@@ -117,6 +117,128 @@ function. This extension function acts as a replacement for Android's
117117
[`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean, callback: BluetoothCallback): BluetoothGatt?`]
118118
method (which relies on a [`BluetoothGattCallback`]).
119119

120+
### Prerequisites
121+
122+
**Able** expects that Android Bluetooth Low Energy is supported
123+
([`BluetoothAdapter.getDefaultAdapter()`] returns non-`null`) and usage prerequisites
124+
(e.g. [bluetooth permissions]) are satisfied prior to use; failing to do so will result in
125+
[`RemoteException`] for most **Able** methods.
126+
127+
## Structured Concurrency
128+
129+
Kotlin Coroutines `0.26.0` introduced [structured concurrency].
130+
131+
When establishing a connection (e.g. via
132+
`BluetoothDevice.connectGatt(context: Context, autoConnect: Boolean): ConnectGattResult` extension
133+
function), if the Coroutine is cancelled then the in-flight connection attempt will be cancelled and
134+
corresponding [`BluetoothGatt`] will be closed:
135+
136+
```kotlin
137+
class ExampleActivity : AppCompatActivity(), CoroutineScope {
138+
139+
protected lateinit var job: Job
140+
override val coroutineContext: CoroutineContext
141+
get() = job + Dispatchers.Main
142+
143+
override fun onCreate(savedInstanceState: Bundle?) {
144+
super.onCreate(savedInstanceState)
145+
job = Job()
146+
147+
val bluetoothDevice: BluetoothDevice = TODO("Retrieve a `BluetoothDevice` from a scan.")
148+
149+
findViewById<Button>(R.id.connect_button).setOnClickListener {
150+
launch {
151+
// If Activity is destroyed during connection attempt, then `result` will contain
152+
// `ConnectGattResult.Canceled`.
153+
val result = bluetoothDevice.connectGatt(this@ExampleActivity, autoConnect = false)
154+
155+
// ...
156+
}
157+
}
158+
}
159+
160+
override fun onDestroy() {
161+
super.onDestroy()
162+
job.cancel()
163+
}
164+
}
165+
```
166+
167+
In the above example, the connection process is tied to the Activity lifecycle. If the Activity is
168+
destroyed (e.g. due to device rotation or navigating away from the Activity) then the connection
169+
attempt will be cancelled. If it is desirable that a connection attempt proceed beyond the Activity
170+
lifecycle then the [`launch`] can be executed in the global scope via `GlobalScope.launch`, in which
171+
case the [`Job`] that [`launch`] returns can be used to manually cancel the connection process (when
172+
desired).
173+
174+
Alternatively, if (for example) an app has an Activity specifically designed to handle the
175+
connection process, then Android Architecture Component's [`ViewModel`] can be scoped (via
176+
`CoroutineScope` interface) allowing connection attempts to be tied to the `ViewModel`'s lifecycle:
177+
178+
```kotlin
179+
class ExampleViewModel(application: Application) : AndroidViewModel(application), CoroutineScope {
180+
181+
private val job = Job()
182+
override val coroutineContext: CoroutineContext
183+
get() = job + Dispatchers.Main
184+
185+
fun connect(bluetoothDevice: BluetoothDevice) {
186+
launch {
187+
// If ViewModel is destroyed during connection attempt, then `result` will contain
188+
// `ConnectGattResult.Canceled`.
189+
val result = bluetoothDevice.connectGatt(getApplication(), autoConnect = false)
190+
191+
// ...
192+
}
193+
}
194+
195+
override fun onCleared() {
196+
job.cancel()
197+
}
198+
}
199+
```
200+
201+
Similar to the connection process, after a connection has been established, if a Coroutine is
202+
cancelled then any `Gatt` operation executing within the Coroutine will be cancelled.
203+
204+
However, unlike the `connectGatt` cancellation handling, an established `Gatt` connection will
205+
**not** automatically disconnect or close when the Coroutine executing a `Gatt` operation is
206+
canceled. Special care must be taken to ensure that Bluetooth Low Energy connections are properly
207+
closed when no longer needed, for example:
208+
209+
```kotlin
210+
val gatt: Gatt = TODO("Acquire Gatt via `BluetoothDevice.connectGatt` extension function.")
211+
212+
launch {
213+
try {
214+
gatt.discoverServices()
215+
// todo: Assign desired characteristic to `characteristic` variable.
216+
val value = gatt.readCharacteristic(characteristic).value
217+
gatt.disconnect()
218+
} finally {
219+
gatt.close()
220+
}
221+
}
222+
```
223+
224+
The `Gatt` interface adheres to [`Closeable`] which can simplify the above example by using [`use`]:
225+
226+
```kotlin
227+
val gatt: Gatt = TODO("Acquire Gatt via `BluetoothDevice.connectGatt` extension function.")
228+
229+
gatt.use { // Will close `gatt` if any failures or cancellation occurs.
230+
gatt.discoverServices()
231+
// todo: Assign desired characteristic to `characteristic` variable.
232+
val value = gatt.readCharacteristic(characteristic).value
233+
gatt.disconnect()
234+
}
235+
```
236+
237+
It may be desirable to manage Bluetooth Low Energy connections entirely manually. In which case,
238+
Coroutine [`GlobalScope`] can be used for both the connection process and operations performed while
239+
connected. In which case, the returned `Gatt` object (after successful connection) can be stored and
240+
later used to disconnect **and** close the underlying `BluetoothGatt`.
241+
120242
## Example
121243

122244
The following code snippet makes a connection to a [`BluetoothDevice`], reads a characteristic,
@@ -179,6 +301,16 @@ limitations under the License.
179301
[`BluetoothGattCallback`]: https://developer.android.com/reference/android/bluetooth/BluetoothGattCallback.html
180302
[`BluetoothDevice`]: https://developer.android.com/reference/android/bluetooth/BluetoothDevice.html
181303
[`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)
304+
[`RemoteException`]: https://developer.android.com/reference/android/os/RemoteException
182305
[Coroutines]: https://kotlinlang.org/docs/reference/coroutines.html
306+
[`Closeable`]: https://docs.oracle.com/javase/7/docs/api/java/io/Closeable.html
307+
[`GlobalScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-global-scope/
183308
[suspension functions]: https://kotlinlang.org/docs/reference/coroutines.html#suspending-functions
309+
[structured concurrency]: https://medium.com/@elizarov/structured-concurrency-722d765aa952
310+
[bluetooth permissions]: https://developer.android.com/guide/topics/connectivity/bluetooth#Permissions
311+
[`Job`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-job/index.html
312+
[`launch`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/launch.html
313+
[`ViewModel`]: https://developer.android.com/topic/libraries/architecture/viewmodel
314+
[`use`]: https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/use.html
315+
[`BluetoothAdapter.getDefaultAdapter()`]: https://developer.android.com/reference/android/bluetooth/BluetoothAdapter#getDefaultAdapter()
184316
[Recipes]: documentation/RECIPES.md

build.gradle

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ buildscript {
1919

2020
'able': getVersionName(),
2121

22-
'kotlin': '1.2.51',
23-
'coroutines': '0.24.0',
22+
'kotlin': '1.2.61',
23+
'coroutines': '0.26.1',
2424

2525
'kotlinTestJunit': '1.2.51',
2626
'mockitoKotlin': '2.0.0-RC1',
@@ -53,6 +53,7 @@ allprojects {
5353
subprojects {
5454
tasks.withType(Test) {
5555
testLogging {
56+
// For more verbosity add: "standardOut", "standardError"
5657
events "passed", "skipped", "failed"
5758
exceptionFormat "full"
5859
showExceptions true

core/src/main/java/messenger/Messenger.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import com.juul.able.experimental.messenger.Message.ReadCharacteristic
1111
import com.juul.able.experimental.messenger.Message.RequestMtu
1212
import com.juul.able.experimental.messenger.Message.WriteCharacteristic
1313
import com.juul.able.experimental.messenger.Message.WriteDescriptor
14+
import kotlinx.coroutines.experimental.GlobalScope
1415
import kotlinx.coroutines.experimental.channels.actor
1516
import kotlinx.coroutines.experimental.channels.consumeEach
1617
import kotlinx.coroutines.experimental.newSingleThreadContext
@@ -23,7 +24,7 @@ class Messenger internal constructor(
2324
internal suspend fun send(message: Message) = actor.send(message)
2425

2526
private val context = newSingleThreadContext("Gatt")
26-
private val actor = actor<Message>(context) {
27+
private val actor = GlobalScope.actor<Message>(context) {
2728
Able.verbose { "Begin" }
2829
consumeEach { message ->
2930
Able.debug { "Waiting for Gatt" }

throw/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
Adds extension functions that `throw` exceptions on failures for various BLE operations.
44

55
```kotlin
6-
fun connect(context: Context, device: BluetoothDevice) = launch(UI) {
6+
fun connect(context: Context, device: BluetoothDevice) = launch {
77
device.connectGattOrThrow(context, autoConnect = false).use { gatt ->
88
gatt.discoverServicesOrThrow()
99

1010
val characteristic = gatt
1111
.getService("F000AA80-0451-4000-B000-000000000000".toUuid())!!
1212
.getCharacteristic("F000AA83-0451-4000-B000-000000000000".toUuid())
1313
val value = gatt.readCharacteristicOrThrow(characteristic).value
14+
println("value = $value")
1415

1516
gatt.disconnect()
1617
}

0 commit comments

Comments
 (0)