Merge "Make address type public" into androidx-main
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothDeviceTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothDeviceTest.kt
index 1fdf9ef..f182513 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothDeviceTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothDeviceTest.kt
@@ -55,7 +55,7 @@
         Assume.assumeNotNull(bluetoothAdapter) // Bluetooth is not available if adapter is null
         val fwkBluetoothDevice = bluetoothAdapter!!.getRemoteDevice("00:01:02:03:04:05")
 
-        val bluetoothDevice = BluetoothDevice(fwkBluetoothDevice)
+        val bluetoothDevice = BluetoothDevice.of(fwkBluetoothDevice)
 
         assertEquals(bluetoothDevice.bondState, fwkBluetoothDevice.bondState)
         assertEquals(bluetoothDevice.name, fwkBluetoothDevice.name)
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothGattDescriptorTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothGattDescriptorTest.kt
deleted file mode 100644
index cdee1e0..0000000
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothGattDescriptorTest.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.bluetooth
-
-import android.bluetooth.BluetoothGattDescriptor as FwkBluetoothGattDescriptor
-import java.util.UUID
-import org.junit.Assert
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.junit.runners.JUnit4
-
-@RunWith(JUnit4::class)
-class BluetoothGattDescriptorTest {
-    @Test
-    fun constructorWithFwkInstance() {
-        val permissionMap = mapOf(
-            FwkBluetoothGattDescriptor.PERMISSION_READ to
-                BluetoothGattDescriptor.PERMISSION_READ,
-            FwkBluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED to
-                BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED,
-            FwkBluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM to
-                BluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM,
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE to
-                BluetoothGattDescriptor.PERMISSION_WRITE,
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED to
-                BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED,
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM to
-                BluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM,
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_SIGNED to
-                BluetoothGattDescriptor.PERMISSION_WRITE_SIGNED,
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_SIGNED_MITM to
-                BluetoothGattDescriptor.PERMISSION_WRITE_SIGNED_MITM
-        )
-
-        permissionMap.forEach {
-            val descUuid = UUID.randomUUID()
-            val fwkGattDescriptor = FwkBluetoothGattDescriptor(descUuid, it.key)
-            val gattDescriptor = BluetoothGattDescriptor(fwkGattDescriptor)
-
-            Assert.assertEquals(fwkGattDescriptor.uuid, gattDescriptor.uuid)
-            Assert.assertEquals(it.value, gattDescriptor.permissions)
-        }
-    }
-}
\ No newline at end of file
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/GattCharacteristicTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/GattCharacteristicTest.kt
index 2d75ccc..0dbc5e5 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/GattCharacteristicTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/GattCharacteristicTest.kt
@@ -17,7 +17,6 @@
 package androidx.bluetooth
 
 import android.bluetooth.BluetoothGattCharacteristic as FwkBluetoothGattCharacteristic
-import android.bluetooth.BluetoothGattDescriptor as FwkBluetoothGattDescriptor
 import java.util.UUID
 import org.junit.Assert
 import org.junit.Test
@@ -85,25 +84,6 @@
             Assert.assertEquals(fwkGattCharacteristic.uuid, gattCharacteristic.uuid)
             Assert.assertEquals(it.value, gattCharacteristic.permissions)
         }
-
-        val charUuid = UUID.randomUUID()
-        val fwkGattCharacteristic = FwkBluetoothGattCharacteristic(
-            charUuid,
-            /*properties=*/0, /*permissions=*/0
-        )
-        val descUuid1 = UUID.randomUUID()
-        val descUuid2 = UUID.randomUUID()
-
-        val desc1 = FwkBluetoothGattDescriptor(descUuid1, /*permission=*/0)
-        val desc2 = FwkBluetoothGattDescriptor(descUuid2, /*permission=*/0)
-        fwkGattCharacteristic.addDescriptor(desc1)
-        fwkGattCharacteristic.addDescriptor(desc2)
-
-        val characteristicWithDescriptors = GattCharacteristic(fwkGattCharacteristic)
-
-        Assert.assertEquals(2, characteristicWithDescriptors.descriptors.size)
-        Assert.assertEquals(descUuid1, characteristicWithDescriptors.descriptors[0].uuid)
-        Assert.assertEquals(descUuid2, characteristicWithDescriptors.descriptors[1].uuid)
     }
 
     @Test
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
index 694d8d4..b687e5a 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/ScanResultTest.kt
@@ -53,8 +53,8 @@
             timeStampNanos)
         val scanResult = ScanResult(fwkScanResult)
 
-        assertEquals(scanResult.device.name, BluetoothDevice(fwkBluetoothDevice).name)
-        assertEquals(scanResult.device.bondState, BluetoothDevice(fwkBluetoothDevice).bondState)
+        assertEquals(scanResult.device.name, BluetoothDevice.of(fwkBluetoothDevice).name)
+        assertEquals(scanResult.device.bondState, BluetoothDevice.of(fwkBluetoothDevice).bondState)
         assertEquals(scanResult.deviceAddress.address, address)
         assertEquals(scanResult.deviceAddress.addressType,
             BluetoothAddress.ADDRESS_TYPE_RANDOM_STATIC)
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
index db94cc1..3ee9cb4 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
@@ -30,10 +30,14 @@
  * @property bondState the bondState for this BluetoothDevice
  *
  */
-class BluetoothDevice internal constructor(
-    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
-    val fwkDevice: FwkBluetoothDevice
+class BluetoothDevice private constructor(
+    internal val fwkDevice: FwkBluetoothDevice
 ) {
+    internal companion object {
+        fun of(device: FwkBluetoothDevice): BluetoothDevice {
+            return BluetoothDevice(device)
+        }
+    }
     val id: UUID = UUID.randomUUID()
 
     @get:RequiresPermission(
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt
deleted file mode 100644
index e691ff8..0000000
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothGattDescriptor.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * Copyright 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.bluetooth
-
-import android.bluetooth.BluetoothGattDescriptor as FwkBluetoothGattDescriptor
-import androidx.annotation.RestrictTo
-import java.util.UUID
-
-/**
- * Represents a Bluetooth GATT characteristic descriptor
- *
- * GATT descriptors contain additional information and attributes of a GATT characteristic,
- * [GattCharacteristic]. They can be used to describe the characteristic's features or
- * to control certain behaviours of the characteristic.
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY)
-class BluetoothGattDescriptor internal constructor(
-    internal var fwkDescriptor: FwkBluetoothGattDescriptor
-) {
-    companion object {
-        /**
-         * The descriptor is readable
-         */
-        const val PERMISSION_READ: Int = FwkBluetoothGattDescriptor.PERMISSION_READ
-
-        /**
-         * The descriptor is readable if encrypted
-         */
-        const val PERMISSION_READ_ENCRYPTED: Int =
-            FwkBluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED
-
-        /**
-         * The descriptor is readable if person-in-the-middle protection is enabled
-         */
-        const val PERMISSION_READ_ENCRYPTED_MITM: Int =
-            FwkBluetoothGattDescriptor.PERMISSION_READ_ENCRYPTED_MITM
-
-        /**
-         * The descriptor is writable
-         */
-        const val PERMISSION_WRITE: Int = FwkBluetoothGattDescriptor.PERMISSION_WRITE
-
-        /**
-         * The descriptor is writable if encrypted
-         */
-        const val PERMISSION_WRITE_ENCRYPTED: Int =
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED
-
-        /**
-         * The descriptor is writable if person-in-the-middle protection is enabled
-         */
-        const val PERMISSION_WRITE_ENCRYPTED_MITM: Int =
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_ENCRYPTED_MITM
-
-        /**
-         * The descriptor is writable if authentication signature is used
-         */
-        const val PERMISSION_WRITE_SIGNED: Int = FwkBluetoothGattDescriptor.PERMISSION_WRITE_SIGNED
-
-        /**
-         * The descriptor is writable if person-in-the-middle protection is enabled
-         * and authentication signature is used
-         */
-        const val PERMISSION_WRITE_SIGNED_MITM: Int =
-            FwkBluetoothGattDescriptor.PERMISSION_WRITE_SIGNED_MITM
-    }
-
-    /**
-     * The UUID of the descriptor.
-     */
-    val uuid: UUID
-        get() = fwkDescriptor.uuid
-
-    /**
-     * The permissions for the descriptor.
-     *
-     * It is a combination of [PERMISSION_READ], [PERMISSION_READ_ENCRYPTED],
-     * [PERMISSION_READ_ENCRYPTED_MITM], [PERMISSION_WRITE], [PERMISSION_WRITE_ENCRYPTED],
-     * [PERMISSION_WRITE_ENCRYPTED_MITM], [PERMISSION_WRITE_SIGNED],
-     * and [PERMISSION_WRITE_SIGNED_MITM].
-     */
-    val permissions: Int
-        get() = fwkDescriptor.permissions
-}
\ No newline at end of file
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index bd54186c..b346c12 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -28,6 +28,8 @@
 import android.os.ParcelUuid
 import android.util.Log
 import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import java.util.UUID
 import kotlinx.coroutines.cancel
 import kotlinx.coroutines.channels.awaitClose
 import kotlinx.coroutines.flow.Flow
@@ -149,4 +151,79 @@
             bleScanner?.stopScan(callback)
         }
     }
-}
\ No newline at end of file
+
+    /**
+     * Scope for operations as a GATT client role.
+     *
+     * @see connectGatt
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    interface GattClientScope {
+
+        /**
+         * Gets the services discovered from the remote device
+         */
+        fun getServices(): List<GattService>
+
+        /**
+         * Gets the service of the remote device by UUID.
+         *
+         * If multiple instances of the same service exist, the first instance of the service
+         * is returned.
+         */
+        fun getService(uuid: UUID): GattService?
+
+        /**
+         * Reads the given remote characteristic.
+         *
+         * @param characteristic a remote [GattCharacteristic] to read
+         * @return The value of the characteristic
+         */
+        suspend fun readCharacteristic(characteristic: GattCharacteristic):
+            Result<ByteArray>
+
+        /**
+         * Writes the given value to the given remote characteristic.
+         *
+         * @param characteristic a remote [GattCharacteristic] to write
+         * @param value a value to be written.
+         * @param writeType [GattCharacteristic.WRITE_TYPE_DEFAULT],
+         * [GattCharacteristic.WRITE_TYPE_NO_RESPONSE], or
+         * [GattCharacteristic.WRITE_TYPE_SIGNED].
+         * @return the result of the write operation
+         */
+        suspend fun writeCharacteristic(
+            characteristic: GattCharacteristic,
+            value: ByteArray,
+            writeType: Int
+        ): Result<Unit>
+
+        /**
+         * 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)
+    }
+
+    /**
+     * Connects to the GATT server on the remote Bluetooth device and
+     * invokes the given [block] after the connection is made.
+     *
+     * The block may not be run if connection fails.
+     *
+     * @param device a [BluetoothDevice] to connect to
+     * @param block a block of code that is invoked after the connection is made.
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    suspend fun <R> connectGatt(
+        device: BluetoothDevice,
+        block: suspend GattClientScope.() -> R
+    ): R? {
+        return GattClientImpl().connect(context, device, block)
+    }
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
index ad27257..9d8b4f2 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
@@ -51,6 +51,10 @@
         const val PERMISSION_WRITE_SIGNED: Int = BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED
         const val PERMISSION_WRITE_SIGNED_MITM: Int =
             BluetoothGattCharacteristic.PERMISSION_WRITE_SIGNED_MITM
+
+        const val WRITE_TYPE_DEFAULT: Int = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
+        const val WRITE_TYPE_SIGNED: Int = BluetoothGattCharacteristic.WRITE_TYPE_SIGNED
+        const val WRITE_TYPE_NO_RESPONSE: Int = BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE
     }
 
     /**
@@ -71,12 +75,6 @@
     val permissions: Int
         get() = fwkCharacteristic.permissions
 
-    /**
-     * A list of descriptors for the characteristic
-     */
-    val descriptors: List<BluetoothGattDescriptor> =
-        fwkCharacteristic.descriptors.map { BluetoothGattDescriptor(it) }
-
     internal var service: GattService? = null
 }
 
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClientImpl.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClientImpl.kt
new file mode 100644
index 0000000..c1f57dd
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattClientImpl.kt
@@ -0,0 +1,362 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothGatt
+import android.bluetooth.BluetoothGattCallback
+import android.bluetooth.BluetoothGattCharacteristic as FwkCharacteristic
+import android.bluetooth.BluetoothGattDescriptor as FwkDescriptor
+import android.bluetooth.BluetoothGattService as FwkService
+import android.content.Context
+import android.util.Log
+import java.util.UUID
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.job
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+/**
+ * A class for handling operations as a GATT client role.
+ */
+internal class GattClientImpl {
+    private companion object {
+        private const val TAG = "GattClientImpl"
+
+        /**
+         * The maximum ATT size(512) + header(3)
+         */
+        private const val GATT_MAX_MTU = 515
+        private val CCCD_UID = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
+    }
+
+    private sealed interface CallbackResult {
+        class OnCharacteristicRead(
+            val characteristic: GattCharacteristic,
+            val value: ByteArray,
+            val status: Int
+        ) : CallbackResult
+
+        class OnCharacteristicWrite(
+            val characteristic: GattCharacteristic,
+            val status: Int
+        ) : CallbackResult
+
+        class OnDescriptorRead(
+            val descriptor: FwkDescriptor,
+            val value: ByteArray,
+            val status: Int
+        ) : CallbackResult
+
+        class OnDescriptorWrite(
+            val descriptor: FwkDescriptor,
+            val status: Int
+        ) : CallbackResult
+    }
+
+    private interface SubscribeListener {
+        fun onCharacteristicNotification(value: ByteArray)
+        fun finish()
+    }
+
+    /**
+     * A mapping from framework instances to BluetoothX instances.
+     */
+    private class AttributeMap {
+        val services: MutableMap<FwkService, GattService> = mutableMapOf()
+        val characteristics: MutableMap<FwkCharacteristic, GattCharacteristic> =
+            mutableMapOf()
+        fun update(services: List<FwkService>) {
+            this.services.clear()
+            characteristics.clear()
+
+            services.forEach { serv ->
+                this.services[serv] = GattService(serv)
+                serv.characteristics.forEach { char ->
+                    characteristics[char] = GattCharacteristic(char)
+                }
+            }
+        }
+
+        fun getServices(): List<GattService> {
+            return services.values.toList()
+        }
+
+        fun fromFwkService(service: FwkService): GattService? {
+            return services[service]
+        }
+
+        fun fromFwkCharacteristic(characteristic: FwkCharacteristic): GattCharacteristic? {
+            return characteristics[characteristic]
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    suspend fun <R> connect(
+        context: Context,
+        device: BluetoothDevice,
+        block: suspend BluetoothLe.GattClientScope.() -> R
+    ): R? = coroutineScope {
+        val connectResult = CompletableDeferred<Boolean>(parent = coroutineContext.job)
+        val finished = Job(parent = coroutineContext.job)
+        val callbackResultsFlow =
+            MutableSharedFlow<CallbackResult>(extraBufferCapacity = Int.MAX_VALUE)
+        val subscribeMap: MutableMap<FwkCharacteristic, SubscribeListener> = mutableMapOf()
+        val subscribeMutex = Mutex()
+        val attributeMap = AttributeMap()
+
+        val callback = object : BluetoothGattCallback() {
+            override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) {
+                if (newState == BluetoothGatt.STATE_CONNECTED) {
+                    gatt?.requestMtu(GATT_MAX_MTU)
+                } else {
+                    connectResult.complete(false)
+                    // TODO(b/270492198): throw precise exception
+                    finished.completeExceptionally(IllegalStateException("connect failed"))
+                }
+            }
+
+            override fun onMtuChanged(gatt: BluetoothGatt?, mtu: Int, status: Int) {
+                if (status == BluetoothGatt.GATT_SUCCESS) {
+                    gatt?.discoverServices()
+                } else {
+                    connectResult.complete(false)
+                    // TODO(b/270492198): throw precise exception
+                    finished.completeExceptionally(IllegalStateException("mtu request failed"))
+                }
+            }
+
+            override fun onServicesDiscovered(gatt: BluetoothGatt?, status: Int) {
+                gatt?.let {
+                    attributeMap.update(it.services)
+                }
+                connectResult.complete(status == BluetoothGatt.GATT_SUCCESS)
+            }
+
+            override fun onCharacteristicRead(
+                gatt: BluetoothGatt,
+                characteristic: FwkCharacteristic,
+                value: ByteArray,
+                status: Int
+            ) {
+               attributeMap.fromFwkCharacteristic(characteristic)?.let {
+                   callbackResultsFlow.tryEmit(
+                       CallbackResult.OnCharacteristicRead(it, value, status)
+                   )
+               }
+            }
+
+            override fun onCharacteristicWrite(
+                gatt: BluetoothGatt,
+                characteristic: FwkCharacteristic,
+                status: Int
+            ) {
+                attributeMap.fromFwkCharacteristic(characteristic)?.let {
+                    callbackResultsFlow.tryEmit(
+                        CallbackResult.OnCharacteristicWrite(it, status)
+                    )
+                }
+            }
+
+            override fun onDescriptorRead(
+                gatt: BluetoothGatt,
+                descriptor: FwkDescriptor,
+                status: Int,
+                value: ByteArray
+            ) {
+                callbackResultsFlow.tryEmit(
+                    CallbackResult.OnDescriptorRead(descriptor, value, status))
+            }
+
+            override fun onDescriptorWrite(
+                gatt: BluetoothGatt,
+                descriptor: FwkDescriptor,
+                status: Int
+            ) {
+                callbackResultsFlow.tryEmit(CallbackResult.OnDescriptorWrite(descriptor, status))
+            }
+
+            override fun onCharacteristicChanged(
+                gatt: BluetoothGatt,
+                characteristic: FwkCharacteristic,
+                value: ByteArray
+            ) {
+                launch {
+                    subscribeMutex.withLock {
+                        subscribeMap[characteristic]?.onCharacteristicNotification(value)
+                    }
+                }
+            }
+        }
+        val bluetoothGatt = device.fwkDevice.connectGatt(context, /*autoConnect=*/false, callback)
+
+        if (!connectResult.await()) {
+            Log.w(TAG, "Failed to connect to the remote GATT server")
+            return@coroutineScope null
+        }
+        val gattScope = object : BluetoothLe.GattClientScope {
+            val taskMutex = Mutex()
+            suspend fun<R> runTask(block: suspend () -> R): R {
+                taskMutex.withLock {
+                    return block()
+                }
+            }
+
+            override fun getServices(): List<GattService> {
+                return attributeMap.getServices()
+            }
+
+            override fun getService(uuid: UUID): GattService? {
+                return bluetoothGatt.getService(uuid)?.let { attributeMap.fromFwkService(it) }
+            }
+
+            override suspend fun readCharacteristic(characteristic: GattCharacteristic):
+                Result<ByteArray> {
+                return runTask {
+                    bluetoothGatt.readCharacteristic(characteristic.fwkCharacteristic)
+                    val res = takeMatchingResult<CallbackResult.OnCharacteristicRead>(
+                        callbackResultsFlow) {
+                        it.characteristic == characteristic
+                    }
+
+                    if (res.status == BluetoothGatt.GATT_SUCCESS) Result.success(res.value)
+                    else Result.failure(RuntimeException("fail"))
+                }
+            }
+
+            override suspend fun writeCharacteristic(
+                characteristic: GattCharacteristic,
+                value: ByteArray,
+                writeType: Int
+            ): Result<Unit> {
+                return runTask {
+                    bluetoothGatt.writeCharacteristic(
+                        characteristic.fwkCharacteristic, value, writeType)
+                    val res = takeMatchingResult<CallbackResult.OnCharacteristicWrite>(
+                        callbackResultsFlow) {
+                        it.characteristic == characteristic
+                    }
+                    if (res.status == BluetoothGatt.GATT_SUCCESS) Result.success(Unit)
+                    else Result.failure(RuntimeException("fail"))
+                }
+            }
+
+            override fun subscribeToCharacteristic(characteristic: GattCharacteristic):
+                Flow<ByteArray> {
+                val cccd = characteristic.fwkCharacteristic.getDescriptor(CCCD_UID)
+                    ?: return emptyFlow()
+
+                return callbackFlow {
+                    val listener = object : SubscribeListener {
+                        override fun onCharacteristicNotification(value: ByteArray) {
+                            trySend(value)
+                        }
+                        override fun finish() {
+                            cancel("finished")
+                        }
+                    }
+                    if (!registerSubscribeListener(characteristic.fwkCharacteristic, listener)) {
+                        cancel("already subscribed")
+                    }
+
+                    runTask {
+                        bluetoothGatt.setCharacteristicNotification(
+                            characteristic.fwkCharacteristic, /*enable=*/true)
+                        bluetoothGatt.writeDescriptor(
+                            cccd,
+                            FwkDescriptor.ENABLE_NOTIFICATION_VALUE
+                        )
+                        val res = takeMatchingResult<CallbackResult.OnDescriptorWrite>(
+                            callbackResultsFlow) {
+                            it.descriptor == cccd
+                        }
+                        if (res.status != BluetoothGatt.GATT_SUCCESS) {
+                            cancel(CancellationException("failed to set notification"))
+                        }
+                    }
+
+                    this.awaitClose {
+                        launch {
+                            unregisterSubscribeListener(characteristic.fwkCharacteristic)
+                        }
+                        bluetoothGatt.setCharacteristicNotification(
+                            characteristic.fwkCharacteristic, /*enable=*/false)
+                        bluetoothGatt.writeDescriptor(
+                            cccd,
+                            FwkDescriptor.DISABLE_NOTIFICATION_VALUE
+                        )
+                    }
+                }
+            }
+
+            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
+            ): Boolean {
+                subscribeMutex.withLock {
+                    if (subscribeMap.containsKey(characteristic)) {
+                        return false
+                    }
+                    subscribeMap[characteristic] = callback
+                    return true
+                }
+            }
+
+            private suspend fun unregisterSubscribeListener(
+                characteristic: FwkCharacteristic
+            ) {
+                subscribeMutex.withLock {
+                    subscribeMap.remove(characteristic)
+                }
+            }
+        }
+        gattScope.block()
+    }
+
+    private suspend inline fun<reified R : CallbackResult> takeMatchingResult(
+        flow: SharedFlow<CallbackResult>,
+        crossinline predicate: (R) -> Boolean
+    ): R {
+        return flow.filter { it is R && predicate(it) }.first() as R
+    }
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
index 9f133b3..dc2dd2f 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
@@ -38,7 +38,7 @@
 class ScanResult internal constructor(private val fwkScanResult: FwkScanResult) {
     /** Remote Bluetooth device found. */
     val device: BluetoothDevice
-        get() = BluetoothDevice(fwkScanResult.device)
+        get() = BluetoothDevice.of(fwkScanResult.device)
 
     // TODO(kihongs) Find a way to get address type from framework scan result
     /** Bluetooth address for the remote device found. */
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
index 86fc8f5..74f4e10 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
@@ -16,11 +16,9 @@
 
 package androidx.bluetooth.integration.testapp.data.connection
 
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattService
-import android.bluetooth.BluetoothGattCharacteristic
-import android.bluetooth.BluetoothGattService
 import androidx.bluetooth.BluetoothDevice
+import androidx.bluetooth.GattCharacteristic
+import androidx.bluetooth.GattService
 import java.util.UUID
 import kotlinx.coroutines.Job
 
@@ -31,15 +29,15 @@
     var onClickReadCharacteristic: OnClickCharacteristic? = null
     var onClickWriteCharacteristic: OnClickCharacteristic? = null
     var status = Status.DISCONNECTED
-    var services = emptyList<BluetoothGattService>()
+    var services = emptyList<GattService>()
 
     private val values = mutableMapOf<UUID, ByteArray?>()
 
-    fun storeValueFor(characteristic: BluetoothGattCharacteristic, value: ByteArray?) {
+    fun storeValueFor(characteristic: GattCharacteristic, value: ByteArray?) {
         values[characteristic.uuid] = value
     }
 
-    fun valueFor(characteristic: BluetoothGattCharacteristic): ByteArray? {
+    fun valueFor(characteristic: GattCharacteristic): ByteArray? {
         return values[characteristic.uuid]
     }
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/OnClickCharacteristic.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/OnClickCharacteristic.kt
index fa5133f..95203a4 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/OnClickCharacteristic.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/OnClickCharacteristic.kt
@@ -16,9 +16,8 @@
 
 package androidx.bluetooth.integration.testapp.data.connection
 
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
-import android.bluetooth.BluetoothGattCharacteristic
+import androidx.bluetooth.GattCharacteristic
 
 interface OnClickCharacteristic {
-    fun onClick(deviceConnection: DeviceConnection, characteristic: BluetoothGattCharacteristic)
+    fun onClick(deviceConnection: DeviceConnection, characteristic: GattCharacteristic)
 }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServiceCharacteristicsAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServiceCharacteristicsAdapter.kt
index 7804498..c0fc9b93 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServiceCharacteristicsAdapter.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServiceCharacteristicsAdapter.kt
@@ -16,14 +16,13 @@
 
 package androidx.bluetooth.integration.testapp.ui.scanner
 
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
-import android.bluetooth.BluetoothGattCharacteristic
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.Button
 import android.widget.LinearLayout
 import android.widget.TextView
+import androidx.bluetooth.GattCharacteristic
 import androidx.bluetooth.integration.testapp.R
 import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
 import androidx.bluetooth.integration.testapp.data.connection.OnClickCharacteristic
@@ -33,7 +32,7 @@
 
 class DeviceServiceCharacteristicsAdapter(
     private val deviceConnection: DeviceConnection,
-    private val characteristics: List<BluetoothGattCharacteristic>,
+    private val characteristics: List<GattCharacteristic>,
     private val onClickReadCharacteristic: OnClickCharacteristic,
     private val onClickWriteCharacteristic: OnClickCharacteristic
 ) : RecyclerView.Adapter<DeviceServiceCharacteristicsAdapter.ViewHolder>() {
@@ -72,7 +71,7 @@
             itemView.findViewById(R.id.button_write_characteristic)
 
         private var currentDeviceConnection: DeviceConnection? = null
-        private var currentCharacteristic: BluetoothGattCharacteristic? = null
+        private var currentCharacteristic: GattCharacteristic? = null
 
         init {
             buttonReadCharacteristic.setOnClickListener {
@@ -92,7 +91,7 @@
             }
         }
 
-        fun bind(deviceConnection: DeviceConnection, characteristic: BluetoothGattCharacteristic) {
+        fun bind(deviceConnection: DeviceConnection, characteristic: GattCharacteristic) {
             currentDeviceConnection = deviceConnection
             currentCharacteristic = characteristic
 
@@ -103,19 +102,19 @@
 
             val propertiesList = mutableListOf<String>()
             // TODO(ofy) Update these with BluetoothGattCharacteristic.isReadable, isWriteable, ...
-            if (properties.and(BluetoothGattCharacteristic.PROPERTY_INDICATE) != 0) {
+            if (properties.and(GattCharacteristic.PROPERTY_INDICATE) != 0) {
                 propertiesList.add(context.getString(R.string.indicate))
             }
-            if (properties.and(BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) {
+            if (properties.and(GattCharacteristic.PROPERTY_NOTIFY) != 0) {
                 propertiesList.add(context.getString(R.string.notify))
             }
-            val isReadable = properties.and(BluetoothGattCharacteristic.PROPERTY_READ) != 0
+            val isReadable = properties.and(GattCharacteristic.PROPERTY_READ) != 0
             if (isReadable) {
                 propertiesList.add(context.getString(R.string.read))
             }
-            val isWriteable = (properties.and(BluetoothGattCharacteristic.PROPERTY_WRITE) != 0 ||
-                properties.and(BluetoothGattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0 ||
-                properties.and(BluetoothGattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
+            val isWriteable = (properties.and(GattCharacteristic.PROPERTY_WRITE) != 0 ||
+                properties.and(GattCharacteristic.PROPERTY_WRITE_NO_RESPONSE) != 0 ||
+                properties.and(GattCharacteristic.PROPERTY_SIGNED_WRITE) != 0)
             if (isWriteable) {
                 propertiesList.add(context.getString(R.string.write))
             }
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServicesAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServicesAdapter.kt
index d7829b1..7c8cd55 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServicesAdapter.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/DeviceServicesAdapter.kt
@@ -16,12 +16,11 @@
 
 package androidx.bluetooth.integration.testapp.ui.scanner
 
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattService
-import android.bluetooth.BluetoothGattService
 import android.view.LayoutInflater
 import android.view.View
 import android.view.ViewGroup
 import android.widget.TextView
+import androidx.bluetooth.GattService
 import androidx.bluetooth.integration.testapp.R
 import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
 import androidx.bluetooth.integration.testapp.data.connection.OnClickCharacteristic
@@ -61,7 +60,7 @@
         private val recyclerViewServiceCharacteristic: RecyclerView =
             itemView.findViewById(R.id.recycler_view_service_characteristic)
 
-        fun bind(deviceConnection: DeviceConnection, service: BluetoothGattService) {
+        fun bind(deviceConnection: DeviceConnection, service: GattService) {
             textViewUuid.text = service.uuid.toString()
 
             recyclerViewServiceCharacteristic.adapter = DeviceServiceCharacteristicsAdapter(
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
index 64dc6bf..ca23455 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -17,9 +17,7 @@
 package androidx.bluetooth.integration.testapp.ui.scanner
 
 // TODO(ofy) Migrate to androidx.bluetooth.AdvertiseParams
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
 import android.annotation.SuppressLint
-import android.bluetooth.BluetoothGattCharacteristic
 import android.os.Bundle
 import android.util.Log
 import android.view.LayoutInflater
@@ -31,12 +29,12 @@
 import androidx.appcompat.app.AlertDialog
 import androidx.bluetooth.BluetoothDevice
 import androidx.bluetooth.BluetoothLe
+import androidx.bluetooth.GattCharacteristic
 import androidx.bluetooth.integration.testapp.R
 import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
 import androidx.bluetooth.integration.testapp.data.connection.OnClickCharacteristic
 import androidx.bluetooth.integration.testapp.data.connection.Status
 import androidx.bluetooth.integration.testapp.databinding.FragmentScannerBinding
-import androidx.bluetooth.integration.testapp.experimental.BluetoothLe as ExperimentalLe
 import androidx.bluetooth.integration.testapp.ui.common.getColor
 import androidx.bluetooth.integration.testapp.ui.common.toast
 import androidx.core.view.isVisible
@@ -64,8 +62,6 @@
     }
 
     private lateinit var bluetoothLe: BluetoothLe
-    // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
-    private lateinit var experimenalLe: ExperimentalLe
 
     private var deviceServicesAdapter: DeviceServicesAdapter? = null
 
@@ -113,7 +109,7 @@
     private val onClickReadCharacteristic = object : OnClickCharacteristic {
         override fun onClick(
             deviceConnection: DeviceConnection,
-            characteristic: BluetoothGattCharacteristic
+            characteristic: GattCharacteristic
         ) {
             deviceConnection.onClickReadCharacteristic?.onClick(deviceConnection, characteristic)
         }
@@ -122,7 +118,7 @@
     private val onClickWriteCharacteristic = object : OnClickCharacteristic {
         override fun onClick(
             deviceConnection: DeviceConnection,
-            characteristic: BluetoothGattCharacteristic
+            characteristic: GattCharacteristic
         ) {
             deviceConnection.onClickWriteCharacteristic?.onClick(deviceConnection, characteristic)
         }
@@ -146,7 +142,6 @@
         super.onViewCreated(view, savedInstanceState)
 
         bluetoothLe = BluetoothLe(requireContext())
-        experimenalLe = ExperimentalLe(requireContext())
 
         binding.tabLayout.addOnTabSelectedListener(onTabSelectedListener)
 
@@ -262,8 +257,7 @@
             }
 
             try {
-                experimenalLe.connectGatt(requireContext(),
-                    deviceConnection.bluetoothDevice.fwkDevice) {
+                bluetoothLe.connectGatt(deviceConnection.bluetoothDevice) {
                     Log.d(TAG, "connectGatt result: getServices() = ${getServices()}")
 
                     deviceConnection.status = Status.CONNECTED
@@ -278,7 +272,7 @@
                         object : OnClickCharacteristic {
                             override fun onClick(
                                 deviceConnection: DeviceConnection,
-                                characteristic: BluetoothGattCharacteristic
+                                characteristic: GattCharacteristic
                             ) {
                                 connectScope.launch {
                                     val result = readCharacteristic(characteristic)
@@ -301,7 +295,7 @@
                         object : OnClickCharacteristic {
                             override fun onClick(
                                 deviceConnection: DeviceConnection,
-                                characteristic: BluetoothGattCharacteristic
+                                characteristic: GattCharacteristic
                             ) {
                                 val view = layoutInflater.inflate(
                                     R.layout.dialog_write_characteristic,
@@ -321,7 +315,7 @@
                                             val result = writeCharacteristic(
                                                 characteristic,
                                                 value,
-                                                BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT
+                                                GattCharacteristic.WRITE_TYPE_DEFAULT
                                             )
                                             Log.d(TAG, "writeCharacteristic() called with: " +
                                                 "result = $result")