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

KeepAliveGatt EventHandler #74

Merged
merged 35 commits into from
Jul 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
716dea5
KeepAliveGatt now takes in a onEventAction, allowing the consumer of …
sdonn3 Jul 9, 2020
72baad9
merge changes from travis' PR
sdonn3 Jul 9, 2020
aaa739d
Some changes to the README to reflect changes in KeepAliveGatt
sdonn3 Jul 9, 2020
785c709
Changes from PR
sdonn3 Jul 10, 2020
934c824
Add API changes
sdonn3 Jul 10, 2020
fe650b1
test format
sdonn3 Jul 10, 2020
7598997
Merge branch 'develop' into steve/esc-965_onDisconnect
sdonn3 Jul 10, 2020
21f6320
fix test
sdonn3 Jul 10, 2020
0adefb9
Merge remote-tracking branch 'origin/steve/esc-965_onDisconnect' into…
sdonn3 Jul 10, 2020
1e1aa35
changes
sdonn3 Jul 13, 2020
acd9291
PR feedback requests
sdonn3 Jul 14, 2020
76b07d1
connectionAttempt fix
sdonn3 Jul 14, 2020
29109b5
Code clean up
twyatt Jul 15, 2020
2368bd6
Drop Disconnected.Info and put properties directly on Disconnected event
twyatt Jul 15, 2020
2ae24d4
API dump
twyatt Jul 15, 2020
c04b2cd
Kotlinter
twyatt Jul 15, 2020
c2e9421
Add test that assumes connection rejection should yield Disconnected …
twyatt Jul 15, 2020
4ef5191
Code clean up and add Rejected event
twyatt Jul 15, 2020
8c8b54c
Add documentation re: states and events
twyatt Jul 15, 2020
803cdad
Simplify example in documentation
twyatt Jul 15, 2020
b1f4654
API dump
twyatt Jul 15, 2020
d26fafd
Kotlinter
twyatt Jul 15, 2020
0b80726
Reduce size of state and event flow diagram
twyatt Jul 15, 2020
521af43
Merge branch 'develop' into steve/esc-965_onDisconnect
twyatt Jul 15, 2020
6421846
Fix reconnect test
twyatt Jul 15, 2020
05ba36e
Settle on disconnected state on exception in onConnected
twyatt Jul 16, 2020
bec6b43
Fix which scope is cancelled due to onConnected exception
twyatt Jul 16, 2020
fe24455
Remove test that is no longer valid
twyatt Jul 16, 2020
560802d
Remove nested scopes and launch in `establishConnection`
twyatt Jul 16, 2020
70357d7
Minor documentation updates
twyatt Jul 16, 2020
f3e0e91
couple new tests
sdonn3 Jul 16, 2020
cb9b61d
Unwrap special exception for Disconnected event
twyatt Jul 16, 2020
c0e38ee
Test clean up
twyatt Jul 16, 2020
045f845
Update state and event flow diagram
twyatt Jul 16, 2020
a89e741
Properly propagate failure through Disconnected state
twyatt Jul 17, 2020
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
52 changes: 45 additions & 7 deletions keep-alive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ fun CoroutineScope.keepAliveGatt(
| `androidContext` | The Android `Context` for establishing Bluetooth Low-Energy connections. |
| `bluetoothDevice` | `BluetoothDevice` to maintain a connection with. |
| `disconnectTimeoutMillis` | Duration (in milliseconds) to wait for connection to gracefully spin down (after `disconnect`) before forcefully closing. |
| `onConnectAction` | Actions to perform upon connection. `Connected` state is propagated _after_ `onConnectAction` completes. |
| `eventHandler` | Actions to perform on various events within the KeepAliveGatt's lifecycle (e.g. Connected, Disconnected, Rejected). |

For example, to create a `KeepAliveGatt` as a child of Android's `viewModelScope`:

Expand All @@ -34,10 +34,7 @@ class ExampleViewModel(application: Application) : AndroidViewModel(application)
application,
bluetoothAdapter.getRemoteDevice(MAC_ADDRESS),
disconnectTimeoutMillis = 5_000L // 5 seconds
) {
// Actions to perform on initial connect *and* subsequent reconnects:
discoverServicesOrThrow()
}
)

fun connect() {
gatt.connect()
Expand Down Expand Up @@ -73,9 +70,50 @@ Once `Connected`, if the connection drops, then `KeepAliveGatt` will automatical
_To disconnect an established connection or cancel an in-flight connection attempt, `disconnect` can
be called (it will suspend until underlying [`BluetoothGatt`] has disconnected)._

### Connection State
## Status

The status of a `KeepAliveGatt` can be monitored via either `Event`s or `State`s. The major
distinction between the two is:

> **`State`**: `State`s are propagated over conflated data streams. If states are changing quickly,
> then some `State`s may be missed (skipped over). For this reason, they're useful for informing a
> user of the current state of the connection; as missing a state is acceptable since subsequent
> states will overwrite the currently reflected state anyways. `State`s should **not** be used if a
> specific condition (e.g. `Connected`) needs to trigger an action (use `Event` instead).

> **`Event`**: `Event`s allow a developer to integrate actions into the connection process. When an
> `Event` is triggered, the connection process is paused (suspended) until processing of the `Event`
> is complete.

`State`s and `Event`s occur in the following order:

![State and event flow](artwork/state-and-event-flow.png)

### Events

`Event`s are configured via the `eventHandler` argument of the `keepAliveGatt` extension function,
for example:

```kotlin
val gatt = viewModelScope.keepAliveGatt(...) { event ->
event.onConnected {
// Actions to perform on initial connect *and* subsequent reconnects:
discoverServicesOrThrow()
}
event.onDisconnected {
// todo: retry strategy (e.g. exponentially increasing delay)
}
}
```

Any uncaught Exceptions in the event handler are propagated up the Coroutine scope that
`keepAliveGatt` extension function was called on (`viewModelScope` in the example above), and cause
the `KeepAliveGatt` to disconnect and **not** reconnect (`connect` can be called to have
`KeepAliveGatt` attempt to reach a Connected state again).

### State

The state can be monitored via the `state` [`Flow`] property:
Connection state can be monitored via the `state` [`Flow`] property:

```kotlin
val gatt = scope.keepAliveGatt(...)
Expand Down
43 changes: 42 additions & 1 deletion keep-alive/api/keep-alive.api
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,48 @@ public final class com/juul/able/keepalive/BuildConfig {
public fun <init> ()V
}

public final class com/juul/able/keepalive/ConnectionRejected : java/io/IOException {
public abstract class com/juul/able/keepalive/Event {
}

public final class com/juul/able/keepalive/Event$Connected : com/juul/able/keepalive/Event {
public fun <init> (Lcom/juul/able/gatt/GattIo;)V
public final fun component1 ()Lcom/juul/able/gatt/GattIo;
public final fun copy (Lcom/juul/able/gatt/GattIo;)Lcom/juul/able/keepalive/Event$Connected;
public static synthetic fun copy$default (Lcom/juul/able/keepalive/Event$Connected;Lcom/juul/able/gatt/GattIo;ILjava/lang/Object;)Lcom/juul/able/keepalive/Event$Connected;
public fun equals (Ljava/lang/Object;)Z
public final fun getGatt ()Lcom/juul/able/gatt/GattIo;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/juul/able/keepalive/Event$Disconnected : com/juul/able/keepalive/Event {
public fun <init> (ZI)V
public final fun component1 ()Z
public final fun component2 ()I
public final fun copy (ZI)Lcom/juul/able/keepalive/Event$Disconnected;
public static synthetic fun copy$default (Lcom/juul/able/keepalive/Event$Disconnected;ZIILjava/lang/Object;)Lcom/juul/able/keepalive/Event$Disconnected;
public fun equals (Ljava/lang/Object;)Z
public final fun getConnectionAttempt ()I
public final fun getWasConnected ()Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/juul/able/keepalive/Event$Rejected : com/juul/able/keepalive/Event {
public fun <init> (Ljava/lang/Throwable;)V
public final fun component1 ()Ljava/lang/Throwable;
public final fun copy (Ljava/lang/Throwable;)Lcom/juul/able/keepalive/Event$Rejected;
public static synthetic fun copy$default (Lcom/juul/able/keepalive/Event$Rejected;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/juul/able/keepalive/Event$Rejected;
public fun equals (Ljava/lang/Object;)Z
public final fun getCause ()Ljava/lang/Throwable;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/juul/able/keepalive/EventKt {
public static final fun onConnected (Lcom/juul/able/keepalive/Event;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun onDisconnected (Lcom/juul/able/keepalive/Event;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public static final fun onRejected (Lcom/juul/able/keepalive/Event;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}

public final class com/juul/able/keepalive/KeepAliveGatt : com/juul/able/gatt/GattIo {
Expand Down
Binary file added keep-alive/artwork/state-and-event-flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
47 changes: 47 additions & 0 deletions keep-alive/src/main/java/Event.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2020 JUUL Labs, Inc.
*/

package com.juul.able.keepalive

import com.juul.able.gatt.GattIo

sealed class Event {

/** Triggered upon a connection being successfully established. */
data class Connected(val gatt: GattIo) : Event()

/**
* Triggered either immediately after an established connection has dropped or after a failed
* connection attempt.
*
* The [connectionAttempt] property represents which connection attempt iteration over the
* lifespan of the [KeepAliveGatt]. The value begins at 1 and increase by 1 for each iteration.
*
* @param wasConnected is `true` if event follows an established connection, or `false` if previous connection attempt failed.
* @param connectionAttempt is the number of connection attempts since creation of [KeepAliveGatt].
*/
data class Disconnected(
val wasConnected: Boolean,
val connectionAttempt: Int
) : Event()

/**
* Triggered when the connection request was rejected by the operating system (e.g. bluetooth
* hardware unavailable). [KeepAliveGatt] will not attempt to reconnect until
* [connect][KeepAliveGatt.connect] is called again.
*/
data class Rejected(val cause: Throwable) : Event()
}

suspend fun Event.onConnected(action: suspend GattIo.() -> Unit) {
if (this is Event.Connected) action.invoke(gatt)
}

suspend fun Event.onDisconnected(action: suspend (Event.Disconnected) -> Unit) {
if (this is Event.Disconnected) action.invoke(this)
}

suspend fun Event.onRejected(action: suspend (Event.Rejected) -> Unit) {
if (this is Event.Rejected) action.invoke(this)
}
Loading