Merge "Cleans up GattClient" into androidx-main
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
index 7bab0cd..ac2372c 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattClientTest.kt
@@ -40,6 +40,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -109,23 +110,22 @@
acceptConnect()
- Assert.assertEquals(true, bluetoothLe.connectGatt(device) {
+ bluetoothLe.connectGatt(device) {
Assert.assertEquals(sampleServices.size, getServices().size)
sampleServices.forEachIndexed { index, service ->
Assert.assertEquals(service.uuid, getServices()[index].uuid)
}
- awaitClose { closed.complete(Unit) }
- true
- }.getOrNull())
+ closed.complete(Unit)
+ }
- Assert.assertTrue(closed.isCompleted)
+ assertTrue(closed.isCompleted)
}
@Test
fun connectFail() = runTest {
val device = createDevice("00:11:22:33:44:55")
rejectConnect()
- Assert.assertEquals(true, bluetoothLe.connectGatt(device) { true }.isFailure)
+ assertTrue(runCatching { bluetoothLe.connectGatt(device) { } }.isFailure)
}
@Test
@@ -155,11 +155,9 @@
readCharacteristic(
getServices()[0].getCharacteristic(readCharUuid)!!
).getOrNull()?.toInt())
- awaitClose {
- closed.complete(Unit)
- }
+ closed.complete(Unit)
}
- Assert.assertTrue(closed.isCompleted)
+ assertTrue(closed.isCompleted)
}
@Test
@@ -175,7 +173,7 @@
bluetoothLe.connectGatt(device) {
Assert.assertEquals(sampleServices.size, getServices().size)
- Assert.assertTrue(
+ assertTrue(
readCharacteristic(
getServices()[0].getCharacteristic(noPropertyCharUuid)!!
).exceptionOrNull()
@@ -228,11 +226,9 @@
valueToWrite.toByteArray())
Assert.assertEquals(valueToWrite,
readCharacteristic(characteristic).getOrNull()?.toInt())
- awaitClose {
- closed.complete(Unit)
- }
+ closed.complete(Unit)
}
- Assert.assertTrue(closed.isCompleted)
+ assertTrue(closed.isCompleted)
}
@Test
@@ -248,7 +244,7 @@
bluetoothLe.connectGatt(device) {
Assert.assertEquals(sampleServices.size, getServices().size)
- Assert.assertTrue(
+ assertTrue(
writeCharacteristic(
getServices()[0].getCharacteristic(readCharUuid)!!,
48.toByteArray()
@@ -305,11 +301,9 @@
subscribeToCharacteristic(characteristic).first().toInt())
Assert.assertEquals(valueToNotify,
readCharacteristic(characteristic).getOrNull()?.toInt())
- awaitClose {
- closed.complete(Unit)
- }
+ closed.complete(Unit)
}
- Assert.assertTrue(closed.isCompleted)
+ assertTrue(closed.isCompleted)
}
@Test
diff --git a/bluetooth/bluetooth/api/current.txt b/bluetooth/bluetooth/api/current.txt
index 1a50302..004a624 100644
--- a/bluetooth/bluetooth/api/current.txt
+++ b/bluetooth/bluetooth/api/current.txt
@@ -63,13 +63,12 @@
public final class BluetoothLe {
ctor public BluetoothLe(android.content.Context context);
method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
+ method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
}
public static interface BluetoothLe.GattClientScope {
- method public suspend Object? awaitClose(kotlin.jvm.functions.Function0<kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
method public java.util.List<androidx.bluetooth.GattService> getServices();
method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
diff --git a/bluetooth/bluetooth/api/restricted_current.txt b/bluetooth/bluetooth/api/restricted_current.txt
index 1a50302..004a624 100644
--- a/bluetooth/bluetooth/api/restricted_current.txt
+++ b/bluetooth/bluetooth/api/restricted_current.txt
@@ -63,13 +63,12 @@
public final class BluetoothLe {
ctor public BluetoothLe(android.content.Context context);
method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
- method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
+ method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super R>);
method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
}
public static interface BluetoothLe.GattClientScope {
- method public suspend Object? awaitClose(kotlin.jvm.functions.Function0<kotlin.Unit> block, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public androidx.bluetooth.GattService? getService(java.util.UUID uuid);
method public java.util.List<androidx.bluetooth.GattService> getServices();
method public suspend Object? readCharacteristic(androidx.bluetooth.GattCharacteristic characteristic, kotlin.coroutines.Continuation<? super kotlin.Result<? extends byte[]>>);
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index 939686f..24d6dd1 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -35,6 +35,7 @@
import androidx.annotation.VisibleForTesting
import java.util.UUID
import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancel
@@ -252,12 +253,6 @@
* Returns a _cold_ [Flow] that contains the indicated value of the given characteristic.
*/
fun subscribeToCharacteristic(characteristic: GattCharacteristic): Flow<ByteArray>
-
- /**
- * Suspends the current coroutine until the pending operations are handled and the
- * connection is closed, then it invokes the given [block] before resuming the coroutine.
- */
- suspend fun awaitClose(block: () -> Unit)
}
/**
@@ -269,6 +264,7 @@
* @param device a [BluetoothDevice] to connect to
* @param block a block of code that is invoked after the connection is made
*
+ * @throws CancellationException if connect failed or it's canceled
* @return a result returned by the given block if the connection was successfully finished
* or a failure with the corresponding reason
*
@@ -277,7 +273,7 @@
suspend fun <R> connectGatt(
device: BluetoothDevice,
block: suspend GattClientScope.() -> R
- ): Result<R> {
+ ): R {
return client.connect(device, block)
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
index 290e1a9..d2a173b 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClient.kt
@@ -48,6 +48,7 @@
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
+import kotlinx.coroutines.withTimeout
/**
* A class for handling operations as a GATT client role.
@@ -88,6 +89,8 @@
* The maximum ATT size(512) + header(3)
*/
private const val GATT_MAX_MTU = 515
+
+ private const val CONNECT_TIMEOUT_MS = 30_000L
private val CCCD_UID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
}
@@ -131,7 +134,7 @@
suspend fun <R> connect(
device: BluetoothDevice,
block: suspend BluetoothLe.GattClientScope.() -> R
- ): Result<R> = coroutineScope {
+ ): R = coroutineScope {
val connectResult = CompletableDeferred<Unit>(parent = coroutineContext.job)
val callbackResultsFlow =
MutableSharedFlow<CallbackResult>(extraBufferCapacity = Int.MAX_VALUE)
@@ -144,7 +147,7 @@
if (newState == BluetoothGatt.STATE_CONNECTED) {
fwkAdapter.requestMtu(GATT_MAX_MTU)
} else {
- connectResult.cancel("connect failed")
+ cancel("connect failed")
}
}
@@ -152,14 +155,14 @@
if (status == BluetoothGatt.GATT_SUCCESS) {
fwkAdapter.discoverServices()
} else {
- connectResult.cancel("mtu request failed")
+ cancel("mtu request failed")
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
attributeMap.updateWithFrameworkServices(fwkAdapter.getServices())
if (status == BluetoothGatt.GATT_SUCCESS) connectResult.complete(Unit)
- else connectResult.cancel("service discover failed")
+ else cancel("service discover failed")
}
override fun onCharacteristicRead(
@@ -219,13 +222,11 @@
}
}
if (!fwkAdapter.connectGatt(context, device.fwkDevice, callback)) {
- return@coroutineScope Result.failure(CancellationException("failed to connect"))
+ throw CancellationException("failed to connect")
}
- try {
+ withTimeout(CONNECT_TIMEOUT_MS) {
connectResult.await()
- } catch (e: Throwable) {
- return@coroutineScope Result.failure(e)
}
val gattScope = object : BluetoothLe.GattClientScope {
val taskMutex = Mutex()
@@ -339,19 +340,6 @@
}
}
- override suspend fun awaitClose(block: () -> Unit) {
- try {
- // Wait for queued tasks done
- taskMutex.withLock {
- subscribeMutex.withLock {
- subscribeMap.values.forEach { it.finish() }
- }
- }
- } finally {
- block()
- }
- }
-
private suspend fun registerSubscribeListener(
characteristic: FwkCharacteristic,
callback: SubscribeListener
@@ -373,11 +361,7 @@
}
}
}
- try {
- Result.success(gattScope.block())
- } catch (e: CancellationException) {
- Result.failure(e)
- }
+ gattScope.block()
}
private suspend inline fun <reified R : CallbackResult> takeMatchingResult(