From 44eadf2fe2bd72fd368b399eb9edd4067aba6bf2 Mon Sep 17 00:00:00 2001 From: Travis Wyatt Date: Tue, 31 Mar 2020 09:58:52 -0700 Subject: [PATCH] Drop device and retry modules _Hopefully_ be reimplementing the functionality they provided after upcoming library redesign. --- .circleci/config.yml | 4 +- README.md | 4 - codecov.yml | 2 - device/.gitignore | 1 - device/README.md | 87 ------- device/build.gradle | 29 --- device/gradle.properties | 2 - device/src/main/AndroidManifest.xml | 5 - device/src/main/java/CoroutinesGattDevice.kt | 244 ------------------ device/src/main/java/CoroutinesGattDevices.kt | 26 -- device/src/main/java/DeviceManager.kt | 40 --- .../src/main/java/android/BluetoothDevice.kt | 86 ------ device/src/test/java/ExampleUnitTest.kt | 20 -- retry/.gitignore | 1 - retry/README.md | 19 -- retry/build.gradle | 29 --- retry/gradle.properties | 2 - retry/src/main/AndroidManifest.xml | 5 - retry/src/main/java/Retry.kt | 189 -------------- retry/src/test/java/ExampleUnitTest.kt | 20 -- settings.gradle | 2 - 21 files changed, 2 insertions(+), 815 deletions(-) delete mode 100644 device/.gitignore delete mode 100644 device/README.md delete mode 100644 device/build.gradle delete mode 100644 device/gradle.properties delete mode 100644 device/src/main/AndroidManifest.xml delete mode 100644 device/src/main/java/CoroutinesGattDevice.kt delete mode 100644 device/src/main/java/CoroutinesGattDevices.kt delete mode 100644 device/src/main/java/DeviceManager.kt delete mode 100644 device/src/main/java/android/BluetoothDevice.kt delete mode 100644 device/src/test/java/ExampleUnitTest.kt delete mode 100644 retry/.gitignore delete mode 100644 retry/README.md delete mode 100644 retry/build.gradle delete mode 100644 retry/gradle.properties delete mode 100644 retry/src/main/AndroidManifest.xml delete mode 100644 retry/src/main/java/Retry.kt delete mode 100644 retry/src/test/java/ExampleUnitTest.kt diff --git a/.circleci/config.yml b/.circleci/config.yml index 1b5c39a..ed3f0ae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,7 +7,7 @@ jobs: steps: - checkout - restore_cache: - key: gradle-{{ checksum "settings.gradle" }}-{{ checksum "build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "processor/build.gradle" }}-{{ checksum "retry/build.gradle" }}-{{ checksum "throw/build.gradle" }}-{{ checksum "timber-logger/build.gradle" }} + key: gradle-{{ checksum "settings.gradle" }}-{{ checksum "build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "processor/build.gradle" }}-{{ checksum "throw/build.gradle" }}-{{ checksum "timber-logger/build.gradle" }} - run: name: Android Assemble, Check command: >- @@ -28,7 +28,7 @@ jobs: - save_cache: paths: - ~/.gradle - key: gradle-{{ checksum "settings.gradle" }}-{{ checksum "build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "processor/build.gradle" }}-{{ checksum "retry/build.gradle" }}-{{ checksum "throw/build.gradle" }}-{{ checksum "timber-logger/build.gradle" }} + key: gradle-{{ checksum "settings.gradle" }}-{{ checksum "build.gradle" }}-{{ checksum "core/build.gradle" }}-{{ checksum "processor/build.gradle" }}-{{ checksum "throw/build.gradle" }}-{{ checksum "timber-logger/build.gradle" }} - run: name: Codecov command: bash <(curl -s https://codecov.io/bash) -t $CODECOV_TOKEN diff --git a/README.md b/README.md index 44e4199..44a7649 100644 --- a/README.md +++ b/README.md @@ -198,10 +198,8 @@ dependencies { | Package | Functionality | |-------------------|-----------------------------------------------------------------------------------------------------------------| | [`processor`] | A `Processor` adds the ability to process (and optionally modify) GATT data
pre-write or post-read. | -| [`retry`] | `Retry` wraps a `Gatt` to add I/O retry functionality and on-demand connection
establishment. | | [`throw`] | Adds extension functions that `throw` exceptions on failures for various BLE
operations. | | [`timber-logger`] | Routes **Able** logging through [Timber](https://github.com/JakeWharton/timber). | -| [`device`] | Provides `BluetoothDevice` extension functions as a single access point for
connectivity and communication. | # License @@ -234,7 +232,5 @@ limitations under the License. [structured concurrency]: https://medium.com/@elizarov/structured-concurrency-722d765aa952 [`CoroutineScope`]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-scope/ [`processor`]: processor -[`retry`]: retry [`throw`]: throw [`timber-logger`]: timber-logger -[`device`]: device diff --git a/codecov.yml b/codecov.yml index b7bc1db..f7ece7d 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,7 +1,5 @@ fixes: - "com/juul/able/experimental::" - - "com/juul/able/experimental/device::" - "com/juul/able/experimental/processor::" - - "com/juul/able/experimental/retry::" - "com/juul/able/experimental/throwable::" - "com/juul/able/experimental/logger/timber::" diff --git a/device/.gitignore b/device/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/device/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/device/README.md b/device/README.md deleted file mode 100644 index bc99496..0000000 --- a/device/README.md +++ /dev/null @@ -1,87 +0,0 @@ -# Device - -Provides [`BluetoothDevice`] extension functions to simplify Android Bluetooth Low Energy usage. -Allows [`BluetoothDevice`] to be used as the single access point for connectivity and communication -without having to manage underlying [`BluetoothGatt`] objects (_error handling omitted for -simplicity_): - -```kotlin -fun connectAndReadEveryMinute( - context: Context, - bluetoothDevice: BluetoothDevice, - characteristic: BluetoothGattCharacteristic -): Job = launch { - while (isActive) { - bluetoothDevice.connect(context) - bluetoothDevice.discoverServices() - - val value = bluetoothDevice.readCharacteristic(characteristic).value - - try { - bluetoothDevice.disconnect() - } finally { - bluetoothDevice.close() - } - - delay(60_000L) - } -} -``` - -Under the hood, [`BluetoothDevice`]s are wrapped and stored in a `ConcurrentMap` so that connection -and characteristic observation `Channel`s can be used across connections (_error handling omitted -for simplicity_): - -```kotlin -fun connectionStateExample(context: Context, bluetoothDevice: BluetoothDevice) { - launch { - // The same `Channel` will persist across connections, no need to resubscribe on reconnect. - bluetoothDevice.onConnectionStateChange.consumeEach { - println("Connection state changed to $it for $bluetoothDevice") - } - } - - launch { - bluetoothDevice.connect(context) - bluetoothDevice.discoverServices() - - delay(10_000L) - - bluetoothDevice.disconnect() - - delay(5_000L) - - bluetoothDevice.connect() - - delay(10_000L) - - try { - bluetoothDevice.disconnect() - } finally { - bluetoothDevice.close() - } - } -} -``` - -When a [`BluetoothDevice`] is no longer needed, it should be disposed from the underlying -`ConcurrentMap`: - -```kotlin -// Close underlying BluetoothGatt as well as connection state and characteristic change Channels. -CoroutinesGattDevices -= bluetoothDevice -``` - -# Setup - -## Gradle - -```groovy -dependencies { - implementation "com.juul.able:device:0.8.0" -} -``` - - -[`BluetoothDevice`]: https://developer.android.com/reference/android/bluetooth/BluetoothDevice -[`BluetoothGatt`]: https://developer.android.com/reference/android/bluetooth/BluetoothGatt diff --git a/device/build.gradle b/device/build.gradle deleted file mode 100644 index 595f7a1..0000000 --- a/device/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply from: rootProject.file('gradle/jacoco-android.gradle') - -kotlin.experimental.coroutines 'enable' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion versions.minSdk - } -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} - -artifacts { - archives sourcesJar -} - -dependencies { - api project(':core') - testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${versions.kotlinTestJunit}" -} - -apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/device/gradle.properties b/device/gradle.properties deleted file mode 100644 index 4e0f9ab..0000000 --- a/device/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -POM_NAME=Able Device -POM_ARTIFACT_ID=device diff --git a/device/src/main/AndroidManifest.xml b/device/src/main/AndroidManifest.xml deleted file mode 100644 index 4c86139..0000000 --- a/device/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/device/src/main/java/CoroutinesGattDevice.kt b/device/src/main/java/CoroutinesGattDevice.kt deleted file mode 100644 index b41098f..0000000 --- a/device/src/main/java/CoroutinesGattDevice.kt +++ /dev/null @@ -1,244 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.device - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothGattService -import android.bluetooth.BluetoothProfile -import android.content.Context -import com.juul.able.experimental.Able -import com.juul.able.experimental.ConnectGattResult -import com.juul.able.experimental.Gatt -import com.juul.able.experimental.GattState -import com.juul.able.experimental.GattStatus -import com.juul.able.experimental.WriteType -import com.juul.able.experimental.android.connectGatt -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Deferred -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel.Factory.CONFLATED -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import java.util.UUID -import java.util.concurrent.atomic.AtomicInteger -import kotlin.coroutines.CoroutineContext - -class GattUnavailable(message: String) : IllegalStateException(message) - -class CoroutinesGattDevice internal constructor( - private val bluetoothDevice: BluetoothDevice -) : Gatt, CoroutineScope { - - /* - * Constructor must **not** have side-effects as we're relying on `ConcurrentMap.getOrPut` - * in `DeviceManager.wrapped` which uses `putIfAbsent` as an alternative to `computeIfAbsent` - * (`computeIfAbsent` is only available on API >= 24). - * - * As stated in [Equivalent of ComputeIfAbsent in Java 7](https://stackoverflow.com/a/40665232): - * - * > This is pretty much functionally equivalent the `computeIfAbsent` call in Java 8, with the - * > only difference being that sometimes you construct a `Value` object that never makes it - * > into the map - because another thread put it in first. It never results in returning the - * > wrong object or anything like that - the function consistently returns the right `Value` no - * > matter what, but _if the construction of `Value` has side-effects*_, this may not be - * > acceptable. - */ - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job - - private val connectMutex = Mutex() - private var connectDeferred: Deferred? = null - - private var _gatt: Gatt? = null - private val gatt: Gatt - get() = _gatt - ?: throw GattUnavailable("Gatt unavailable for bluetooth device $bluetoothDevice") - - private val _connectionState = AtomicInteger() - fun getConnectionState(): GattState = _connectionState.get() - - private var eventJob = Job(job) - set(value) { - // Prevent runaways: cancel the previous Job that we are loosing a reference to. - field.cancel() - field = value - } - - /** - * Scopes forwarding events to (i.e. the following `Channel`s are consumed under this scope): - * - * - [onConnectionStateChange] - * - [onCharacteristicChanged] - * - * @see createConnection - */ - private val eventCoroutineScope - get() = CoroutineScope(eventJob) - - override val onConnectionStateChange = BroadcastChannel(CONFLATED) - override val onCharacteristicChanged = BroadcastChannel(1) - - fun isConnected(): Boolean = - _gatt != null && getConnectionState() == BluetoothProfile.STATE_CONNECTED - - suspend fun connect( - context: Context, - autoConnect: Boolean, - callbackConfig: GattCallbackConfig - ): ConnectGattResult { - Able.verbose { "Connection requested to bluetooth device $bluetoothDevice" } - - val result = connectMutex.withLock { - connectDeferred ?: createConnection(context, autoConnect, callbackConfig).also { - connectDeferred = it - } - }.await() - - Able.info { "connect ← result=$result" } - return result - } - - private suspend fun cancelConnect() = connectMutex.withLock { - connectDeferred?.cancel() - ?: Able.verbose { "No connection to cancel for bluetooth device $bluetoothDevice" } - connectDeferred = null - } - - private fun createConnection( - context: Context, - autoConnect: Boolean, - callbackConfig: GattCallbackConfig - ): Deferred = async { - Able.info { "Creating connection for bluetooth device $bluetoothDevice" } - val result = bluetoothDevice.connectGatt(context, autoConnect, callbackConfig) - - if (result is ConnectGattResult.Success) { - val newGatt = result.gatt - _gatt = newGatt - - eventJob = Job(job) // Prepare event Coroutine scope. - - eventCoroutineScope.launch { - Able.verbose { "onConnectionStateChange → $bluetoothDevice → Begin" } - newGatt.onConnectionStateChange.consumeEach { - if (it.status == BluetoothGatt.GATT_SUCCESS) { - _connectionState.set(it.newState) - } - Able.verbose { "Forwarding $it for $bluetoothDevice" } - onConnectionStateChange.send(it) - } - Able.verbose { "onConnectionStateChange ← $bluetoothDevice ← End" } - }.invokeOnCompletion { - Able.verbose { "onConnectionStateChange for $bluetoothDevice completed, cause=$it" } - } - - eventCoroutineScope.launch { - Able.verbose { "onCharacteristicChanged → $bluetoothDevice → Begin" } - newGatt.onCharacteristicChanged.consumeEach { - Able.verbose { "Forwarding $it for $bluetoothDevice" } - onCharacteristicChanged.send(it) - } - Able.verbose { "onCharacteristicChanged ← $bluetoothDevice ← End" } - }.invokeOnCompletion { - Able.verbose { "onCharacteristicChanged for $bluetoothDevice completed, cause=$it" } - } - - ConnectGattResult.Success(this@CoroutinesGattDevice) - } else { - result - } - } - - override val services: List - get() = gatt.services - - override fun requestConnect(): Boolean = gatt.requestConnect() - - override fun requestDisconnect(): Unit = gatt.requestDisconnect() - - override fun getService(uuid: UUID): BluetoothGattService? = gatt.getService(uuid) - - override suspend fun connect(): Boolean = gatt.connect() - - override suspend fun disconnect() { - cancelConnect() - - Able.verbose { "Disconnecting from bluetooth device $bluetoothDevice" } - _gatt?.disconnect() - ?: Able.warn { "Unable to disconnect from bluetooth device $bluetoothDevice" } - - eventJob.cancel() - } - - override fun close() { - Able.verbose { "close → Begin" } - - runBlocking { - cancelConnect() - } - - eventJob.cancel() - - Able.debug { "close → Closing bluetooth device $bluetoothDevice" } - _gatt?.close() - _gatt = null - - Able.verbose { "close ← End" } - } - - internal fun dispose() { - Able.verbose { "dispose → Begin" } - - job.cancel() - _gatt?.close() - - Able.verbose { "dispose ← End" } - } - - override suspend fun discoverServices(): GattStatus = gatt.discoverServices() - - override suspend fun readCharacteristic( - characteristic: BluetoothGattCharacteristic - ): OnCharacteristicRead = gatt.readCharacteristic(characteristic) - - override suspend fun writeCharacteristic( - characteristic: BluetoothGattCharacteristic, - value: ByteArray, - writeType: WriteType - ): OnCharacteristicWrite = gatt.writeCharacteristic(characteristic, value, writeType) - - override suspend fun writeDescriptor( - descriptor: BluetoothGattDescriptor, - value: ByteArray - ): OnDescriptorWrite = gatt.writeDescriptor(descriptor, value) - - override suspend fun requestMtu(mtu: Int): OnMtuChanged = gatt.requestMtu(mtu) - - override fun setCharacteristicNotification( - characteristic: BluetoothGattCharacteristic, - enable: Boolean - ): Boolean = gatt.setCharacteristicNotification(characteristic, enable) - - override fun toString(): String = - "CoroutinesGattDevice(bluetoothDevice=$bluetoothDevice, state=${getConnectionState()})" -} - diff --git a/device/src/main/java/CoroutinesGattDevices.kt b/device/src/main/java/CoroutinesGattDevices.kt deleted file mode 100644 index 7240514..0000000 --- a/device/src/main/java/CoroutinesGattDevices.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.device - -import android.bluetooth.BluetoothDevice -import com.juul.able.experimental.device.android.isConnected - -object CoroutinesGattDevices { - - val deviceManager = DeviceManager() - - fun wrapped(bluetoothDevice: BluetoothDevice): CoroutinesGattDevice = - deviceManager.wrapped(bluetoothDevice) - - fun remove(bluetoothDevice: BluetoothDevice): CoroutinesGattDevice? = - deviceManager.remove(bluetoothDevice) - - operator fun minusAssign(bluetoothDevice: BluetoothDevice) { - deviceManager.minusAssign(bluetoothDevice) - } -} - -val CoroutinesGattDevices.connectedBluetoothDevices - get() = deviceManager.bluetoothDevices.filter { it.isConnected() } diff --git a/device/src/main/java/DeviceManager.kt b/device/src/main/java/DeviceManager.kt deleted file mode 100644 index 7ef197a..0000000 --- a/device/src/main/java/DeviceManager.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.device - -import android.bluetooth.BluetoothDevice -import com.juul.able.experimental.Able -import java.util.concurrent.ConcurrentHashMap - -class DeviceManager { - - private val wrapped = ConcurrentHashMap() - - val bluetoothDevices - get() = wrapped.keys().toList() - - val coroutinesGattDevices - get() = wrapped.values.toList() - - fun wrapped(bluetoothDevice: BluetoothDevice): CoroutinesGattDevice = - wrapped.getOrPut(bluetoothDevice) { - CoroutinesGattDevice(bluetoothDevice) - } - - fun remove(bluetoothDevice: BluetoothDevice): CoroutinesGattDevice? { - val removed = wrapped.remove(bluetoothDevice) - if (removed != null) { - removed.dispose() - } else { - Able.warn { "remove ← Bluetooth device $bluetoothDevice not found" } - } - return removed - } - - operator fun minusAssign(bluetoothDevice: BluetoothDevice) { - remove(bluetoothDevice) - Able.debug { "close ← Remaining: ${wrapped.values}" } - } -} diff --git a/device/src/main/java/android/BluetoothDevice.kt b/device/src/main/java/android/BluetoothDevice.kt deleted file mode 100644 index e4c95b1..0000000 --- a/device/src/main/java/android/BluetoothDevice.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -@file:Suppress("unused") - -package com.juul.able.experimental.device.android - -import android.bluetooth.BluetoothDevice -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT -import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothGattService -import android.content.Context -import com.juul.able.experimental.ConnectGattResult -import com.juul.able.experimental.GattStatus -import com.juul.able.experimental.WriteType -import com.juul.able.experimental.device.CoroutinesGattDevices -import com.juul.able.experimental.messenger.GattCallbackConfig -import com.juul.able.experimental.messenger.OnCharacteristicChanged -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnConnectionStateChange -import com.juul.able.experimental.messenger.OnDescriptorWrite -import com.juul.able.experimental.messenger.OnMtuChanged -import kotlinx.coroutines.channels.BroadcastChannel -import java.util.UUID - -private val BluetoothDevice.coroutinesGattDevice - get() = CoroutinesGattDevices.wrapped(this) - -fun BluetoothDevice.isConnected(): Boolean = coroutinesGattDevice.isConnected() - -suspend fun BluetoothDevice.connect( - context: Context, - autoConnect: Boolean, - callbackConfig: GattCallbackConfig = GattCallbackConfig() -): ConnectGattResult = coroutinesGattDevice.connect(context, autoConnect, callbackConfig) - -val BluetoothDevice.onConnectionStateChange: BroadcastChannel - get() = coroutinesGattDevice.onConnectionStateChange - -val BluetoothDevice.onCharacteristicChanged: BroadcastChannel - get() = coroutinesGattDevice.onCharacteristicChanged - -val BluetoothDevice.services: List - get() = coroutinesGattDevice.services - -fun BluetoothDevice.requestConnect(): Boolean = coroutinesGattDevice.requestConnect() - -fun BluetoothDevice.requestDisconnect(): Unit = coroutinesGattDevice.requestDisconnect() - -fun BluetoothDevice.getService(uuid: UUID): BluetoothGattService? = - coroutinesGattDevice.getService(uuid) - -suspend fun BluetoothDevice.connect(): Boolean = coroutinesGattDevice.connect() - -suspend fun BluetoothDevice.disconnect(): Unit = coroutinesGattDevice.disconnect() - -suspend fun BluetoothDevice.discoverServices(): GattStatus = coroutinesGattDevice.discoverServices() - -suspend fun BluetoothDevice.readCharacteristic( - characteristic: BluetoothGattCharacteristic -): OnCharacteristicRead = coroutinesGattDevice.readCharacteristic(characteristic) - -suspend fun BluetoothDevice.writeCharacteristic( - characteristic: BluetoothGattCharacteristic, - value: ByteArray, - writeType: WriteType = WRITE_TYPE_DEFAULT -) = coroutinesGattDevice.writeCharacteristic(characteristic, value, writeType) - -suspend fun BluetoothDevice.writeDescriptor( - descriptor: BluetoothGattDescriptor, - value: ByteArray -): OnDescriptorWrite = coroutinesGattDevice.writeDescriptor(descriptor, value) - -suspend fun BluetoothDevice.requestMtu(mtu: Int): OnMtuChanged = - coroutinesGattDevice.requestMtu(mtu) - -fun BluetoothDevice.setCharacteristicNotification( - characteristic: BluetoothGattCharacteristic, - enable: Boolean -): Boolean = coroutinesGattDevice.setCharacteristicNotification(characteristic, enable) - -fun BluetoothDevice.close() { - coroutinesGattDevice.close() -} diff --git a/device/src/test/java/ExampleUnitTest.kt b/device/src/test/java/ExampleUnitTest.kt deleted file mode 100644 index 5861230..0000000 --- a/device/src/test/java/ExampleUnitTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.device.test - -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/retry/.gitignore b/retry/.gitignore deleted file mode 100644 index 796b96d..0000000 --- a/retry/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/retry/README.md b/retry/README.md deleted file mode 100644 index aa9cbdf..0000000 --- a/retry/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Retry - -`Retry` wraps a `Gatt` to add I/O retry functionality and on-demand connection establishment. - -```kotlin -val gatt = device - .connectGattOrThrow(context, autoConnect = false) - .withRetry(1L, MINUTES) -``` - -# Setup - -## Gradle - -```groovy -dependencies { - implementation "com.juul.able:retry:0.8.0" -} -``` diff --git a/retry/build.gradle b/retry/build.gradle deleted file mode 100644 index 595f7a1..0000000 --- a/retry/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android' -apply from: rootProject.file('gradle/jacoco-android.gradle') - -kotlin.experimental.coroutines 'enable' - -android { - compileSdkVersion versions.compileSdk - - defaultConfig { - minSdkVersion versions.minSdk - } -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} - -artifacts { - archives sourcesJar -} - -dependencies { - api project(':core') - testImplementation "org.jetbrains.kotlin:kotlin-test-junit:${versions.kotlinTestJunit}" -} - -apply from: rootProject.file('gradle/gradle-mvn-push.gradle') diff --git a/retry/gradle.properties b/retry/gradle.properties deleted file mode 100644 index bf78af5..0000000 --- a/retry/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -POM_NAME=Able Retry -POM_ARTIFACT_ID=retry diff --git a/retry/src/main/AndroidManifest.xml b/retry/src/main/AndroidManifest.xml deleted file mode 100644 index d8c9b4a..0000000 --- a/retry/src/main/AndroidManifest.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/retry/src/main/java/Retry.kt b/retry/src/main/java/Retry.kt deleted file mode 100644 index 8fa6525..0000000 --- a/retry/src/main/java/Retry.kt +++ /dev/null @@ -1,189 +0,0 @@ -/* - * Copyright 2018 JUUL Labs, Inc. - */ - -@file:Suppress("RedundantUnitReturnType") - -package com.juul.able.experimental.retry - -import android.bluetooth.BluetoothGatt.GATT_SUCCESS -import android.bluetooth.BluetoothGattCharacteristic -import android.bluetooth.BluetoothGattDescriptor -import android.bluetooth.BluetoothProfile.STATE_CONNECTED -import android.bluetooth.BluetoothProfile.STATE_DISCONNECTED -import com.juul.able.experimental.Able -import com.juul.able.experimental.Gatt -import com.juul.able.experimental.WriteType -import com.juul.able.experimental.messenger.OnCharacteristicRead -import com.juul.able.experimental.messenger.OnCharacteristicWrite -import com.juul.able.experimental.messenger.OnDescriptorWrite -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeout -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean - -fun Gatt.withRetry(timeoutDuration: Long, timeoutUnit: TimeUnit) = - Retry(this, timeoutDuration, timeoutUnit) - -/** - * Wraps a [Gatt] to add I/O retry functionality and on-demand connection establishment. - * - * All I/O operations (e.g. [writeCharacteristic], [readCharacteristic], etc) will: - * - Check that a connection is established - * - If a connection is not established, then it will request a connection - * - Attempt the I/O operation - * - * If I/O operation fails then it will repeat the above process until it is successful or timeout - * occurs. - * - * Explicitly calling [disconnect] or [close] will disable (set [isEnabled] to `false`) this - * [Retry]. When this [Retry] is disabled, it will not repeat the aforementioned process. - * - * Timeout is the allowed duration that an I/O operation can take before timing out. When a timeout - * occurs, a [TimeoutCancellationException] is thrown (see [withTimeout]). - * - * The timeout functionality cannot be disabled (i.e. timeout must be > `0`); alternatively a large - * timeout may be used, such as `Long.MAX_VALUE`. - * - * The timeout is backed by a variable representing timeout in milliseconds, as such, the maximum - * supported timeout is `Long.MAX_VALUE` (2^63 - 1) milliseconds or ~292,471,207.50888 years. - */ -class Retry(private val gatt: Gatt, timeoutDuration: Long, timeoutUnit: TimeUnit) : Gatt by gatt { - - private val timeoutMillis = timeoutUnit.toMillis(timeoutDuration).also { - require(it > 0L) { "Timeout (milliseconds) must be > 0, was $it." } - } - - private val _isEnabled = AtomicBoolean(true) - - /** - * Determines if this [Retry] is enabled. When [isEnabled] is `false`, [checkConnection] will - * not attempt to re-establish connection. - */ - var isEnabled: Boolean - get() = _isEnabled.get() - set(value) = _isEnabled.set(value) - - /** - * Suspends until connection state is [STATE_CONNECTED]. - * - * If [STATE_DISCONNECTED] occurs then [Gatt.connect] will be invoked. - * - * This method will return immediately if [isEnabled] is `false`. - * - * @throws [IllegalStateException] if [Gatt.connect] call returns `false`. - */ - private suspend fun checkConnection(): Unit { - if (!isEnabled) { - return - } - - Able.verbose { "checkConnection → Begin" } - onConnectionStateChange.openSubscription().also { subscription -> - subscription.consumeEach { (_, newState) -> - Able.verbose { "checkConnection → consumeEach → newState = $newState" } - if (!isEnabled) { - subscription.cancel() - return - } - - when (newState) { - STATE_DISCONNECTED -> { - Able.debug { "checkConnection → consumeEach → STATE_DISCONNECTED" } - gatt.requestConnect() || error("`BluetoothGatt.connect()` returned false.") - } - STATE_CONNECTED -> { - Able.debug { "checkConnection → consumeEach → STATE_CONNECTED" } - subscription.cancel() - } - } - Able.verbose { "checkConnection → consumeEach → Repeat" } - } - Able.verbose { "checkConnection → End" } - } - } - - private val lock = Mutex(locked = false) - private suspend fun withTimeoutLock(millis: Long, block: suspend () -> T): T { - return lock.withLock { - withTimeout(millis) { - block() - } - } - } - - override fun requestConnect(): Boolean = gatt.requestConnect().also { isEnabled = true } - - override fun requestDisconnect() { - isEnabled = false - gatt.requestDisconnect() - } - - override fun close(): Unit { - isEnabled = false - gatt.close() - } - - /** - * Reads (and retries if necessary) characteristic, connecting to [Gatt] as needed. - * - * @throws [TimeoutCancellationException] if timeout occurs. - * @throws [IllegalStateException] if [checkConnection] calls [Gatt.connect] and it returns `false`. - */ - override suspend fun readCharacteristic( - characteristic: BluetoothGattCharacteristic - ): OnCharacteristicRead { - return withTimeoutLock(timeoutMillis) { - var result: OnCharacteristicRead - do { - checkConnection() - result = gatt.readCharacteristic(characteristic) - } while (result.status != GATT_SUCCESS && isEnabled) - result - } - } - - /** - * Writes (and retries if necessary) characteristic, connecting to [Gatt] as needed. - * - * @throws [TimeoutCancellationException] if timeout occurs. - * @throws [IllegalStateException] if [checkConnection] calls [Gatt.connect] and it returns `false`. - */ - override suspend fun writeCharacteristic( - characteristic: BluetoothGattCharacteristic, - value: ByteArray, - writeType: WriteType - ): OnCharacteristicWrite { - return withTimeoutLock(timeoutMillis) { - var result: OnCharacteristicWrite - do { - checkConnection() - result = gatt.writeCharacteristic(characteristic, value, writeType) - } while (result.status != GATT_SUCCESS && isEnabled) - result - } - } - - /** - * Writes (and retries if necessary) descriptor, connecting to [Gatt] as needed. - * - * @throws [TimeoutCancellationException] if timeout occurs. - * @throws [IllegalStateException] if [checkConnection] calls [Gatt.connect] and it returns `false`. - */ - override suspend fun writeDescriptor( - descriptor: BluetoothGattDescriptor, - value: ByteArray - ): OnDescriptorWrite { - return withTimeoutLock(timeoutMillis) { - var result: OnDescriptorWrite - do { - checkConnection() - result = gatt.writeDescriptor(descriptor, value) - } while (result.status != GATT_SUCCESS && isEnabled) - result - } - } -} diff --git a/retry/src/test/java/ExampleUnitTest.kt b/retry/src/test/java/ExampleUnitTest.kt deleted file mode 100644 index 59a6df3..0000000 --- a/retry/src/test/java/ExampleUnitTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright 2020 JUUL Labs, Inc. - */ - -package com.juul.able.experimental.retry.test - -import kotlin.test.Test -import kotlin.test.assertEquals - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/settings.gradle b/settings.gradle index fc8f959..5a77a41 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,6 +1,4 @@ include ':core' -include ':device' -include ':retry' include ':processor' include ':throw' include ':timber-logger'