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

Commit

Permalink
Add event handling support to KeepAliveGatt (#74)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdonn3 authored Jul 17, 2020
1 parent 668f7aa commit a6e47cd
Show file tree
Hide file tree
Showing 7 changed files with 634 additions and 165 deletions.
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

0 comments on commit a6e47cd

Please sign in to comment.