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")