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(