[go: nahoru, domu]

Add tests for GattServer

Test coverage for GattServer is 76% with this change.

Bug: 294322777
Test: ./gradlew bluetooth:bluetooth:check \
                bluetooth:bluetooth:connectedCheck \
                bluetooth:bluetooth-testing:check
Change-Id: I983806202ed27d22c1f4f6506fc5bc0812086ef9
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
new file mode 100644
index 0000000..685a859
--- /dev/null
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricGattServerTest.kt
@@ -0,0 +1,332 @@
+/*
+ * 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.testing
+
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothDevice as FwkDevice
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattServer
+import android.bluetooth.BluetoothGattServerCallback
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.content.Context
+import androidx.bluetooth.BluetoothLe
+import androidx.bluetooth.GattCharacteristic
+import androidx.bluetooth.GattCharacteristic.Companion.PERMISSION_READ
+import androidx.bluetooth.GattCharacteristic.Companion.PERMISSION_WRITE
+import androidx.bluetooth.GattCharacteristic.Companion.PROPERTY_NOTIFY
+import androidx.bluetooth.GattCharacteristic.Companion.PROPERTY_READ
+import androidx.bluetooth.GattCharacteristic.Companion.PROPERTY_WRITE
+import androidx.bluetooth.GattServer
+import androidx.bluetooth.GattServerRequest
+import androidx.bluetooth.GattService
+import java.nio.ByteBuffer
+import java.util.UUID
+import junit.framework.TestCase.fail
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+import org.robolectric.Shadows.shadowOf
+import org.robolectric.shadows.ShadowBluetoothGattServer
+
+@RunWith(RobolectricTestRunner::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class RobolectricGattServerTest {
+    private val context: Context = RuntimeEnvironment.getApplication()
+       private val bluetoothManager: BluetoothManager =
+        context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+    private val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
+
+    private lateinit var bluetoothLe: BluetoothLe
+    private lateinit var serverAdapter: StubServerFrameworkAdapter
+
+    private companion object {
+        private val serviceUuid1 = UUID.fromString("00001111-0000-1000-8000-00805F9B34FB")
+        private val serviceUuid2 = UUID.fromString("00001112-0000-1000-8000-00805F9B34FB")
+
+        private val readCharUuid = UUID.fromString("00002221-0000-1000-8000-00805F9B34FB")
+        private val writeCharUuid = UUID.fromString("00002222-0000-1000-8000-00805F9B34FB")
+        private val notifyCharUuid = UUID.fromString("00002223-0000-1000-8000-00805F9B34FB")
+
+        private val cccdUuid = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")
+
+        private val readCharacteristic = GattCharacteristic(
+            readCharUuid,
+            PROPERTY_READ, PERMISSION_READ
+        )
+        private val writeCharacteristic = GattCharacteristic(
+            writeCharUuid,
+            PROPERTY_READ or PROPERTY_WRITE, PERMISSION_READ or PERMISSION_WRITE
+        )
+        private val notifyCharacteristic = GattCharacteristic(
+            notifyCharUuid,
+            PROPERTY_READ or PROPERTY_NOTIFY, PERMISSION_READ
+        )
+
+        private val service1 = GattService(
+            serviceUuid1,
+            listOf(readCharacteristic, writeCharacteristic, notifyCharacteristic)
+        )
+        private val service2 = GattService(serviceUuid2, listOf())
+    }
+
+    @Before
+    fun setUp() {
+        bluetoothLe = BluetoothLe(context)
+        serverAdapter = StubServerFrameworkAdapter(bluetoothLe.server.fwkAdapter)
+        bluetoothLe.server.fwkAdapter = serverAdapter
+    }
+
+    @Test
+    fun openGattServer() = runTest {
+        val device = createDevice("00:11:22:33:44:55")
+        val opened = CompletableDeferred<Unit>()
+        val closed = CompletableDeferred<Unit>()
+
+        serverAdapter.>
+            StubServerFrameworkAdapter.OnOpenGattServerListener {
+                connectDevice(device) { opened.complete(Unit) }
+            }
+        serverAdapter.>
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+
+        bluetoothLe.openGattServer(listOf()).first().accept {
+            Assert.assertEquals(device.address, this.device.address)
+        }
+
+        Assert.assertTrue(opened.isCompleted)
+        Assert.assertTrue(closed.isCompleted)
+    }
+
+    @Test
+    fun openGattServer_readCharacteristic() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+        val valueToRead = 42
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                serverAdapter.callback.onCharacteristicReadRequest(
+                    device, /*requestId=*/1, /*offset=*/0, readCharacteristic.fwkCharacteristic)
+            }
+        }
+        serverAdapter.>
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services).collect {
+                it.accept {
+                    when (val request = requests.first()) {
+                        is GattServerRequest.ReadCharacteristicRequest -> {
+                            request.sendResponse(true, valueToRead.toByteArray())
+                        }
+                        else -> fail("unexpected request")
+                    }
+                    // Close the server
+                    this@launch.cancel()
+                }
+            }
+        }.join()
+
+        // Ensure if the server is closed
+        Assert.assertTrue(closed.isCompleted)
+        Assert.assertEquals(1, serverAdapter.shadowGattServer.responses.size)
+        Assert.assertEquals(valueToRead, serverAdapter.shadowGattServer.responses[0].toInt())
+    }
+
+   @Test
+    fun openGattServer_rejectAndAccept_throwsException() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                serverAdapter.callback.onCharacteristicReadRequest(
+                    device, /*requestId=*/1, /*offset=*/0, readCharacteristic.fwkCharacteristic)
+            }
+        }
+        serverAdapter.>
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services).collect {
+                it.reject()
+                Assert.assertThrows(
+                    IllegalStateException::class.java
+                ) {
+                    runBlocking {
+                        it.accept {}
+                    }
+                }
+                this@launch.cancel()
+            }
+        }.join()
+
+        Assert.assertTrue(closed.isCompleted)
+        Assert.assertEquals(0, serverAdapter.shadowGattServer.responses.size)
+    }
+
+   @Test
+    fun openGattServer_writeCharacteristic() = runTest {
+        val services = listOf(service1, service2)
+        val device = createDevice("00:11:22:33:44:55")
+        val closed = CompletableDeferred<Unit>()
+        val valueToWrite = 42
+
+        runAfterServicesAreAdded(services.size) {
+            connectDevice(device) {
+                serverAdapter.callback.onCharacteristicWriteRequest(
+                    device, /*requestId=*/1, writeCharacteristic.fwkCharacteristic,
+                    /*preparedWrite=*/false, /*responseNeeded=*/false,
+                    /*offset=*/0, valueToWrite.toByteArray())
+            }
+        }
+        serverAdapter.>
+            StubServerFrameworkAdapter.OnCloseGattServerListener {
+                closed.complete(Unit)
+            }
+
+        launch {
+            bluetoothLe.openGattServer(services).collect {
+                it.accept {
+                    when (val request = requests.first()) {
+                        is GattServerRequest.WriteCharacteristicRequest -> {
+                            Assert.assertEquals(valueToWrite, request.value?.toInt())
+                            request.sendResponse(true)
+                        }
+                        else -> fail("unexpected request")
+                    }
+                    // Close the server
+                    this@launch.cancel()
+                }
+            }
+        }.join()
+
+        Assert.assertTrue(closed.isCompleted)
+        Assert.assertEquals(1, serverAdapter.shadowGattServer.responses.size)
+        Assert.assertEquals(valueToWrite, serverAdapter.shadowGattServer.responses[0].toInt())
+    }
+
+    private fun<R> runAfterServicesAreAdded(countServices: Int, block: suspend () -> R) {
+        var waitCount = countServices
+        serverAdapter. {
+            if (--waitCount == 0) {
+                runBlocking {
+                    block()
+                }
+            }
+        }
+    }
+
+    private fun<R> connectDevice(device: FwkDevice, block: () -> R): R {
+        serverAdapter.shadowGattServer.notifyConnection(device)
+        return block()
+    }
+
+    private fun createDevice(address: String): FwkDevice {
+        return bluetoothAdapter!!.getRemoteDevice(address)
+    }
+
+    class StubServerFrameworkAdapter(
+        private val baseAdapter: GattServer.FrameworkAdapter
+    ) : GattServer.FrameworkAdapter {
+        val shadowGattServer: ShadowBluetoothGattServer
+            get() = shadowOf(gattServer)
+        val callback: BluetoothGattServerCallback
+            get() = shadowGattServer.gattServerCallback
+        override var gattServer: BluetoothGattServer?
+            get() = baseAdapter.gattServer
+            set(value) { baseAdapter.gattServer = value }
+
+        var onOpenGattServerListener: OnOpenGattServerListener? = null
+        var onCloseGattServerListener: OnCloseGattServerListener? = null
+        var onAddServiceListener: OnAddServiceListener? = null
+
+        override fun openGattServer(context: Context, callback: BluetoothGattServerCallback) {
+            baseAdapter.openGattServer(context, callback)
+            onOpenGattServerListener?.onOpenGattServer()
+        }
+
+        override fun closeGattServer() {
+            baseAdapter.closeGattServer()
+            onCloseGattServerListener?.onCloseGattServer()
+        }
+
+        override fun clearServices() {
+            baseAdapter.clearServices()
+        }
+
+        override fun addService(service: BluetoothGattService) {
+            baseAdapter.addService(service)
+            onAddServiceListener?.onAddService(service)
+        }
+
+        override fun notifyCharacteristicChanged(
+            device: FwkDevice,
+            characteristic: BluetoothGattCharacteristic,
+            confirm: Boolean,
+            value: ByteArray
+        ) {
+            baseAdapter.notifyCharacteristicChanged(device, characteristic, confirm, value)
+        }
+
+        override fun sendResponse(
+            device: FwkDevice,
+            requestId: Int,
+            status: Int,
+            offset: Int,
+            value: ByteArray?
+        ) {
+            baseAdapter.sendResponse(device, requestId, status, offset, value)
+        }
+
+        fun interface OnOpenGattServerListener {
+            fun onOpenGattServer()
+        }
+        fun interface OnAddServiceListener {
+            fun onAddService(service: BluetoothGattService)
+        }
+       fun interface OnCloseGattServerListener {
+            fun onCloseGattServer()
+        }
+    }
+}
+
+private fun Int.toByteArray(): ByteArray {
+    return ByteBuffer.allocate(Int.SIZE_BYTES).putInt(this).array()
+}
+
+private fun ByteArray.toInt(): Int {
+    return ByteBuffer.wrap(this).int
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index cc95ef8..abf6539 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -53,7 +53,9 @@
     @VisibleForTesting
     @get:RestrictTo(RestrictTo.Scope.LIBRARY)
     val client = GattClient(context)
-    private val server = GattServer(context)
+    @VisibleForTesting
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    val server = GattServer(context)
 
     /**
      * Returns a _cold_ [Flow] to start Bluetooth LE Advertising. When the flow is successfully collected,
@@ -252,7 +254,7 @@
          *
          * @see GattServerScope
          */
-        suspend fun accept(block: GattServerScope.() -> Unit) {
+        suspend fun accept(block: suspend GattServerScope.() -> Unit) {
             return server.acceptConnection(this, block)
         }
 
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
index c198dc9..577d196 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattCharacteristic.kt
@@ -25,7 +25,8 @@
  */
 @RestrictTo(RestrictTo.Scope.LIBRARY)
 class GattCharacteristic internal constructor(
-    internal var fwkCharacteristic: BluetoothGattCharacteristic
+    @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+    var fwkCharacteristic: BluetoothGattCharacteristic
 ) {
     companion object {
         /**
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
index 785983d..293c76b 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/GattServer.kt
@@ -29,6 +29,8 @@
 import android.content.Context
 import android.os.Build
 import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import androidx.annotation.VisibleForTesting
 import java.util.concurrent.atomic.AtomicBoolean
 import java.util.concurrent.atomic.AtomicReference
 import kotlinx.coroutines.channels.Channel
@@ -41,8 +43,10 @@
 /**
  * Class for handling operations as a GATT server role
  */
-internal class GattServer(private val context: Context) {
-    private interface GattServerImpl {
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class GattServer(private val context: Context) {
+    interface FrameworkAdapter {
+        var gattServer: BluetoothGattServer?
         fun openGattServer(context: Context, callback: BluetoothGattServerCallback)
         fun closeGattServer()
         fun clearServices()
@@ -82,9 +86,11 @@
     private val attributeMap = AttributeMap()
 
     @SuppressLint("ObsoleteSdkInt")
-    private val impl: GattServerImpl =
-        if (Build.VERSION.SDK_INT >= 33) GattServerImplApi33()
-        else BaseGattServerImpl()
+    @VisibleForTesting
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    var fwkAdapter: FrameworkAdapter =
+        if (Build.VERSION.SDK_INT >= 33) FrameworkAdapterApi33()
+        else FrameworkAdapterBase()
 
     fun open(services: List<GattService>):
         Flow<BluetoothLe.GattServerConnectionRequest> = callbackFlow {
@@ -155,22 +161,22 @@
                 }
             }
         }
-        impl.openGattServer(context, callback)
-        services.forEach { impl.addService(it.fwkService) }
+        fwkAdapter.openGattServer(context, callback)
+        services.forEach { fwkAdapter.addService(it.fwkService) }
 
         awaitClose {
-            impl.closeGattServer()
+            fwkAdapter.closeGattServer()
         }
     }
 
     fun updateServices(services: List<GattService>) {
-        impl.clearServices()
-        services.forEach { impl.addService(it.fwkService) }
+        fwkAdapter.clearServices()
+        services.forEach { fwkAdapter.addService(it.fwkService) }
     }
 
     suspend fun<R> acceptConnection(
         request: BluetoothLe.GattServerConnectionRequest,
-        block: BluetoothLe.GattServerScope.() -> R
+        block: suspend BluetoothLe.GattServerScope.() -> R
     ) = coroutineScope {
         val session = request.session
         if (!session.state.compareAndSet(Session.State.CONNECTING, Session.State.CONNECTED)) {
@@ -185,7 +191,7 @@
                 characteristic: GattCharacteristic,
                 value: ByteArray
             ) {
-                impl.notifyCharacteristicChanged(
+                fwkAdapter.notifyCharacteristicChanged(
                     request.device.fwkDevice, characteristic.fwkCharacteristic, false, value)
             }
         }
@@ -223,11 +229,11 @@
         offset: Int,
         value: ByteArray?
     ) {
-        impl.sendResponse(device, requestId, status, offset, value)
+        fwkAdapter.sendResponse(device, requestId, status, offset, value)
     }
 
-    private open class BaseGattServerImpl : GattServerImpl {
-        internal var gattServer: BluetoothGattServer? = null
+    private open class FrameworkAdapterBase : FrameworkAdapter {
+        override var gattServer: BluetoothGattServer? = null
         private val isOpen = AtomicBoolean(false)
         @RequiresPermission(BLUETOOTH_CONNECT)
         override fun openGattServer(context: Context, callback: BluetoothGattServerCallback) {
@@ -278,7 +284,7 @@
         }
     }
 
-    private open class GattServerImplApi33 : BaseGattServerImpl() {
+    private open class FrameworkAdapterApi33 : FrameworkAdapterBase() {
         @RequiresPermission(BLUETOOTH_CONNECT)
         override fun notifyCharacteristicChanged(
             device: FwkBluetoothDevice,