diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerEvent.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerEvent.kt index 6ff4571ef..055cf85f2 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerEvent.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerEvent.kt @@ -6,7 +6,7 @@ import uk.gov.onelogin.sharing.bluetooth.internal.core.SessionEndStates sealed interface GattServerEvent { data class Connected(val address: String) : GattServerEvent data class Disconnected(val address: String, val isSessionEnd: Boolean) : GattServerEvent - data class ServiceAdded(val service: BluetoothGattService?) : GattServerEvent + data class ServiceAdded(val service: BluetoothGattService) : GattServerEvent data class MessageReceived(val byteArray: ByteArray) : GattServerEvent { override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerManager.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerManager.kt index d5cdfe008..6eaf7eff0 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerManager.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/gatt/peripheral/GattServerManager.kt @@ -12,5 +12,10 @@ interface GattServerManager : fun open(serviceUuid: UUID) + /** + * Disconnects the currently connected bluetooth device. + */ + fun cancelCurrentConnection() + fun notifySessionEnd(serviceUuid: UUID): SessionEndStateQueued } diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallback.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallback.kt index 193789d1a..254686382 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallback.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallback.kt @@ -163,7 +163,7 @@ class GattServerCallback( } } - override fun onServiceAdded(status: Int, service: BluetoothGattService?) { + override fun onServiceAdded(status: Int, service: BluetoothGattService) { gatGattEventEmitter.emit( GattServerCallbackEvent.ServiceAdded(status, service) ) diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallbackEvent.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallbackEvent.kt index 73cabd85d..c1a5f893f 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallbackEvent.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/GattServerCallbackEvent.kt @@ -33,7 +33,7 @@ sealed interface GattServerCallbackEvent { } } - data class ServiceAdded(val status: Int, val service: BluetoothGattService?) : + data class ServiceAdded(val status: Int, val service: BluetoothGattService) : GattServerCallbackEvent data class MessageReceived(val device: BluetoothDevice, val byteArray: ByteArray) : GattServerCallbackEvent { diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransport.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransport.kt index 797f4c038..d6360c872 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransport.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransport.kt @@ -51,6 +51,9 @@ class AndroidPeripheralBluetoothTransport( internal var monitoringJob: Job = monitorServerEvents() + @Volatile + internal var isServiceReady: Boolean = false + private fun cancelCurrentJobs() { monitoringJob.cancel() monitoringJob = monitorServerEvents() @@ -87,8 +90,13 @@ class AndroidPeripheralBluetoothTransport( ioDispatcher + "$logTag.Start".asCoroutineName() ) { cancelCurrentJobs() + isServiceReady = false _state.value = PeripheralBluetoothState.Idle + gattServerManager.open(serviceUuid) + } + + private suspend fun startAdvertising(serviceUuid: UUID) { monitoringJob.start() bluetoothStateMonitor.start() try { @@ -98,8 +106,6 @@ class AndroidPeripheralBluetoothTransport( _state.value = PeripheralBluetoothState.Error(PeripheralBluetoothTransportError.ADVERTISING_FAILED) } - - gattServerManager.open(serviceUuid) } override suspend fun stop(serviceUuid: UUID, sendEndCommand: Boolean): Unit = @@ -108,9 +114,9 @@ class AndroidPeripheralBluetoothTransport( if (sendEndCommand) { notifySessionEnd(serviceUuid) } + bluetoothStateMonitor.stop() bleAdvertiser.stopAdvertise() gattServerManager.close() - bluetoothStateMonitor.stop() _state.value = PeripheralBluetoothState.Idle } @@ -125,16 +131,57 @@ class AndroidPeripheralBluetoothTransport( } private fun handleAdvertiserState(state: AdvertiserState) { - if (state is AdvertiserState.Failed) { - _state.value = - PeripheralBluetoothState.Error( - PeripheralBluetoothTransportError.ADVERTISING_FAILED - ) + when (state) { + is AdvertiserState.Started -> { + isServiceReady = true + } + + AdvertiserState.Stopping, + is AdvertiserState.Stopped -> { + isServiceReady = false + } + + is AdvertiserState.Failed -> { + _state.value = + PeripheralBluetoothState.Error( + PeripheralBluetoothTransportError.ADVERTISING_FAILED + ) + } + + AdvertiserState.Idle, + AdvertiserState.Starting + -> { + // do nothing with intermediary advertisement states + } } + logger.debug(logTag, "Advertising ${state::class.java.simpleName}") } private fun handleGattEvent(event: GattServerEvent) { + when (event) { + is GattServerEvent.Connected -> { + if (!isServiceReady) { + logger.debug( + logTag, + "Rejecting connection from ${event.address} - service not ready" + ) + gattServerManager.cancelCurrentConnection() + } + } + + is GattServerEvent.ServiceAdded -> { + coroutineScope.launch( + ioDispatcher + "$logTag.StartAdvertising".asCoroutineName() + ) { + startAdvertising(event.service.uuid) + } + } + + else -> { + // don't perform additional logic for other events + } + } event.let(serverEventTransformer::transform)?.let { bluetoothState -> _state.value = bluetoothState }.also { diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/GattServerEventToPeripheralBluetoothState.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/GattServerEventToPeripheralBluetoothState.kt index ee5f99e23..3fc2dc4e4 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/GattServerEventToPeripheralBluetoothState.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/GattServerEventToPeripheralBluetoothState.kt @@ -21,7 +21,7 @@ class GattServerEventToPeripheralBluetoothState(private val logger: Logger) : ) is GattServerEvent.ServiceAdded -> { - logger.debug(logTag, "Service Added: ${source.service?.uuid}") + logger.debug(logTag, "Service Added: ${source.service.uuid}") null } diff --git a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManager.kt b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManager.kt index 19cea9372..223b93687 100644 --- a/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManager.kt +++ b/bluetooth/src/main/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManager.kt @@ -55,7 +55,7 @@ class AndroidGattServerManager( ) override val events: SharedFlow = _events private var gattServer: BluetoothGattServer? = null - private var connectedDevice: BluetoothDevice? = null + internal var connectedDevice: BluetoothDevice? = null @SuppressLint("MissingPermission") private val eventEmitter = GattEventEmitter { @@ -64,12 +64,8 @@ class AndroidGattServerManager( private var mtu = MIN_MTU private var isSessionEnd = false - @Volatile - private var isServiceReady = false - @RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT) override fun open(serviceUuid: UUID) { - isServiceReady = false val gattService = gattServiceFactory(serviceUuid) if (permissionsChecker.checkPermissions(getBluetoothPermissions()).isNotEmpty()) { @@ -108,7 +104,6 @@ class AndroidGattServerManager( gattServer = null connectedDevice = null isSessionEnd = false - isServiceReady = false mtu = MIN_MTU _events.tryEmit(GattServerEvent.ServiceStopped) } @@ -137,6 +132,14 @@ class AndroidGattServerManager( _events.tryEmit(GattServerEvent.MessageReceived(event.byteArray)) } + @SuppressLint("MissingPermission") + override fun cancelCurrentConnection() { + connectedDevice?.let { + gattServer?.cancelConnection(it) + connectedDevice = null + } + } + @SuppressLint("MissingPermission") private fun handleConnectionStateChange(event: GattServerCallbackEvent.ConnectionStateChange) { val address = event.device.address @@ -144,20 +147,8 @@ class AndroidGattServerManager( val event = when { event.status == BluetoothGatt.GATT_SUCCESS && event.newState == BluetoothProfile.STATE_CONNECTED -> { - // Reject connections that arrive before addService() completes. - // This happens when a device from a previous session is still actively connecting - // Android routes it to the new GATT server immediately on openGattServer(), - // before the service is registered. - if (!isServiceReady) { - logger.debug( - logTag, - "Rejecting connection from $address - service not ready" - ) - gattServer?.cancelConnection(event.device) - return - } connectedDevice = event.device - GattServerEvent.Connected(address) + GattServerEvent.Connected(event.device.address) } event.newState == BluetoothProfile.STATE_DISCONNECTED -> { @@ -188,11 +179,8 @@ class AndroidGattServerManager( private fun handleServiceAdded(event: GattServerCallbackEvent.ServiceAdded) { if (event.status == BluetoothGatt.GATT_SUCCESS) { - isServiceReady = true _events.tryEmit( - GattServerEvent.ServiceAdded( - event.service - ) + GattServerEvent.ServiceAdded(event.service) ) } else { logger.error(logTag, "Failed to add service, status: ${event.status}") diff --git a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransportTest.kt b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransportTest.kt index 4f6888442..fce67741b 100644 --- a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransportTest.kt +++ b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/api/peripheral/mdoc/AndroidPeripheralBluetoothTransportTest.kt @@ -1,10 +1,17 @@ package uk.gov.onelogin.sharing.bluetooth.api.peripheral.mdoc +import android.bluetooth.BluetoothGatt.GATT_SUCCESS import android.bluetooth.BluetoothGattService +import android.bluetooth.BluetoothProfile import app.cash.turbine.test +import com.google.testing.junit.testparameterinjector.KotlinTestParameters.testValues +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector import io.mockk.every import io.mockk.mockk import java.util.UUID +import kotlin.test.assertFalse +import kotlin.test.assertTrue import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob @@ -16,6 +23,7 @@ import org.hamcrest.Matchers.equalTo import org.junit.Assert.assertEquals import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith import uk.gov.logging.testdouble.v2.SystemLogger import uk.gov.onelogin.sharing.bluetooth.api.advertising.AdvertiserState import uk.gov.onelogin.sharing.bluetooth.api.advertising.AdvertisingError @@ -31,11 +39,13 @@ import uk.gov.onelogin.sharing.bluetooth.api.peripheral.mdoc.PeripheralBluetooth import uk.gov.onelogin.sharing.bluetooth.ble.DEVICE_ADDRESS import uk.gov.onelogin.sharing.bluetooth.ble.FakeBleAdvertiser import uk.gov.onelogin.sharing.bluetooth.ble.FakeBluetoothStateMonitor +import uk.gov.onelogin.sharing.bluetooth.internal.core.SessionEndStates import uk.gov.onelogin.sharing.bluetooth.internal.peripheral.FakeGattServerManager import uk.gov.onelogin.sharing.core.MainDispatcherRule import uk.gov.onelogin.sharing.core.coroutines.JobMatchers.isActive @OptIn(ExperimentalCoroutinesApi::class) +@RunWith(TestParameterInjector::class) class AndroidPeripheralBluetoothTransportTest { @get:Rule @@ -118,50 +128,148 @@ class AndroidPeripheralBluetoothTransportTest { } @Test - fun `start triggers advertiser start and gatt server open`() = - runTest(testScope.coroutineContext) { - transport.monitoringJob.start() - gattServerManager.emitEvent(GattServerEvent.Connected(DEVICE_ADDRESS)) - advanceUntilIdle() + fun `Starting the transport defers to opening the GATT Server`() = runTest( + dispatcherRule.testDispatcher + ) { + transport.monitoringJob.start() + transport.start(uuid) + advanceUntilIdle() - transport.start(uuid) - advanceUntilIdle() + assertThat( + gattServerManager.openCalls, + equalTo(1) + ) - assertEquals(PeripheralBluetoothState.Idle, transport.state.value) - assertEquals(1, advertiser.startCalls) - assertEquals(uuid, advertiser.lastAdvertiseData?.serviceUuid) - assertEquals(AdvertiserState.Started, advertiser.state.value) - assertEquals(1, gattServerManager.openCalls) - assertEquals(1, bluetoothStateMonitor.startCalls) - } + assertFalse { transport.isServiceReady } + } @Test - fun `start sets Error state when advertiser throws`() = runTest(testScope.coroutineContext) { - val advertiser = FakeBleAdvertiser().apply { - exceptionToThrow = StartAdvertisingException(AdvertisingError.INTERNAL_ERROR) + fun `Begins advertising when receiving 'ServiceAdded' events`() = runTest( + dispatcherRule.testDispatcher + ) { + assertFalse { transport.isServiceReady } + transport.monitoringJob.start() + val service: BluetoothGattService = mockk() + every { service.uuid } returns uuid + + val event = GattServerEvent.ServiceAdded(service) + gattServerManager.emitEvent(event) + advanceUntilIdle() + + advertiser.state.test { + assertThat( + expectMostRecentItem(), + equalTo(AdvertiserState.Started) + ) } - val sessionManager = AndroidPeripheralBluetoothTransport( - bleAdvertiser = advertiser, - gattServerManager = gattServerManager, - bluetoothStateMonitor = bluetoothStateMonitor, - coroutineScope = testScope, - logger = logger + assertTrue { transport.isServiceReady } + assertTrue { "Completed handling gatt server event: $event" in logger } + } + + @Test + fun `Advertising state changes are logged`( + @TestParameter event: AdvertiserState = testValues( + AdvertiserState.Idle, + AdvertiserState.Stopped, + AdvertiserState.Stopping, + AdvertiserState.Failed("error"), + AdvertiserState.Starting, + AdvertiserState.Started ) + ) = runTest(dispatcherRule.testDispatcher) { + transport.monitoringJob.start() + advertiser.emitState(event) + advanceUntilIdle() - sessionManager.state.test { - assertEquals(PeripheralBluetoothState.Idle, awaitItem()) + assertTrue { "Advertising ${event::class.java.simpleName}" in logger } + } - sessionManager.start(uuid) - advanceUntilIdle() + @Test + fun `GATT Server event changes are logged`( + @TestParameter event: GattServerEvent = testValues( + GattServerEvent.Connected(uuid.toString()), + GattServerEvent.Disconnected(uuid.toString(), false), + GattServerEvent.ServiceAdded(mockk(relaxed = true)), + GattServerEvent.MessageReceived(byteArrayOf()), + GattServerEvent.SessionStarted, + GattServerEvent.ServiceStopped, + GattServerEvent.Error(GattServerError.GATT_NOT_AVAILABLE), + GattServerEvent.SessionEnd(SessionEndStates.SUCCESS), + GattServerEvent.UnsupportedEvent( + DEVICE_ADDRESS, + GATT_SUCCESS, + BluetoothProfile.STATE_CONNECTED + ) + ) + ) = runTest(dispatcherRule.testDispatcher) { + transport.monitoringJob.start() + gattServerManager.emitEvent(event) + advanceUntilIdle() - assertEquals( - PeripheralBluetoothState.Error( - PeripheralBluetoothTransportError.ADVERTISING_FAILED - ), - awaitItem() + assertTrue { "Completed handling gatt server event: $event" in logger } + } + + @Test + fun `Stopping advertising marks the service as 'not ready'`( + @TestParameter event: AdvertiserState = testValues( + AdvertiserState.Stopping, + AdvertiserState.Stopped + ) + ) = runTest(dispatcherRule.testDispatcher) { + transport.isServiceReady = true + transport.monitoringJob.start() + + advertiser.emitState(event) + advanceUntilIdle() + + assertFalse { transport.isServiceReady } + } + + @Test + fun `Disregards connections when the service isn't ready`() = runTest( + dispatcherRule.testDispatcher + ) { + transport.isServiceReady = false + transport.monitoringJob.start() + gattServerManager.emitEvent(GattServerEvent.Connected(uuid.toString())) + advanceUntilIdle() + + assertTrue { + "Rejecting connection from $uuid - service not ready" in logger + } + assertThat( + gattServerManager.disconnectCalls, + equalTo(1) + ) + } + + @Test + fun `Advertiser exceptions emit an error event`( + @TestParameter advertisingError: AdvertisingError + ) = runTest(dispatcherRule.testDispatcher) { + advertiser.exceptionToThrow = StartAdvertisingException(advertisingError) + transport.monitoringJob.start() + + val service: BluetoothGattService = mockk() + every { service.uuid } returns uuid + gattServerManager.emitEvent(GattServerEvent.ServiceAdded(service)) + advanceUntilIdle() + + transport.state.test { + assertThat( + expectMostRecentItem(), + equalTo( + PeripheralBluetoothState.Error( + PeripheralBluetoothTransportError.ADVERTISING_FAILED + ) + ) ) } + + assertTrue { + logger.any { it.message.startsWith("Error starting advertising") } + } } @Test @@ -378,4 +486,14 @@ class AndroidPeripheralBluetoothTransportTest { ) } } + + @Test + fun `Starting the transport after the service is ready marks it as unready`() = runTest( + dispatcherRule.testDispatcher + ) { + transport.isServiceReady = true + transport.start(uuid) + + assertFalse { transport.isServiceReady } + } } diff --git a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManagerTest.kt b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManagerTest.kt index 60b811d34..6ebfb5194 100644 --- a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManagerTest.kt +++ b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/AndroidGattServerManagerTest.kt @@ -20,6 +20,9 @@ import io.mockk.slot import io.mockk.verify import java.util.UUID import kotlin.test.Test +import kotlin.test.assertNull +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Before @@ -40,6 +43,7 @@ import uk.gov.onelogin.sharing.prerequisites.api.permissions.PermissionChecker import uk.gov.onelogin.sharing.prerequisites.permissions.FakePermissionChecker import uk.gov.onelogin.sharing.prerequisites.permissions.PermissionsToResultExt.toDeniedPermission +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("LargeClass") class AndroidGattServerManagerTest { private val context = mockk(relaxed = true) @@ -146,68 +150,6 @@ class AndroidGattServerManagerTest { } } - @Test - fun `rejects connection when service is not ready`() = runTest { - val (callbackSlot, gattServer) = setupOpenGattServer(bluetoothManager, context) - manager.open(uuid) - - manager.events.test { - callbackSlot.captured.onConnectionStateChange( - device, - BluetoothGatt.GATT_SUCCESS, - BluetoothProfile.STATE_CONNECTED - ) - - expectNoEvents() - } - - verify { gattServer.cancelConnection(device) } - } - - @Test - fun `rejects stale connection after close and re-open until service is ready`() = runTest { - val (callbackSlot, gattServer) = setupOpenGattServer(bluetoothManager, context) - manager.open(uuid) - - // Phase 1: Normal session — service ready, connection accepted - callbackSlot.captured.onServiceAdded(BluetoothGatt.GATT_SUCCESS, fakeGattService) - callbackSlot.captured.onConnectionStateChange( - device, - BluetoothGatt.GATT_SUCCESS, - BluetoothProfile.STATE_CONNECTED - ) - - // Phase 2: Session cancelled — close and re-open - manager.close() - manager.open(uuid) - - // Phase 3: Stale connection arrives before service is ready → rejected - manager.events.test { - callbackSlot.captured.onConnectionStateChange( - device, - BluetoothGatt.GATT_SUCCESS, - BluetoothProfile.STATE_CONNECTED - ) - expectNoEvents() - } - verify { gattServer.cancelConnection(device) } - - // Phase 4: Service registers → new connection now accepted - manager.events.test { - callbackSlot.captured.onServiceAdded(BluetoothGatt.GATT_SUCCESS, fakeGattService) - awaitItem() // ServiceAdded event - - callbackSlot.captured.onConnectionStateChange( - device, - BluetoothGatt.GATT_SUCCESS, - BluetoothProfile.STATE_CONNECTED - ) - assertEquals(GattServerEvent.Connected(DEVICE_ADDRESS), awaitItem()) - - cancelAndIgnoreRemainingEvents() - } - } - @Test fun `ignores disconnection when no connection was accepted`() = runTest { val (callbackSlot) = setupOpenGattServer(bluetoothManager, context) @@ -284,30 +226,6 @@ class AndroidGattServerManagerTest { } } - @Test - fun `rejects connection when service added with failure status`() = runTest { - val (callbackSlot, gattServer) = setupOpenGattServer(bluetoothManager, context) - manager.open(uuid) - - manager.events.test { - callbackSlot.captured.onServiceAdded(BluetoothGatt.GATT_FAILURE, fakeGattService) - assertEquals( - GattServerEvent.Error(GattServerError.SERVICE_REGISTRATION_FAILED), - awaitItem() - ) - - callbackSlot.captured.onConnectionStateChange( - device, - BluetoothGatt.GATT_SUCCESS, - BluetoothProfile.STATE_CONNECTED - ) - - expectNoEvents() - } - - verify { gattServer.cancelConnection(device) } - } - @Test fun `emits session state started event`() = runTest { val (callbackSlot) = setupOpenGattServer(bluetoothManager, context) @@ -790,4 +708,26 @@ class AndroidGattServerManagerTest { assert(!result) } + + @Test + fun `Cannot cancel a connection without knowing about the device`() = runTest { + val (callbackSlot, gattServer) = setupOpenGattServer(bluetoothManager, context) + manager.open(uuid) + advanceUntilIdle() + + callbackSlot.captured.onServiceAdded(BluetoothGatt.GATT_SUCCESS, fakeGattService) + + callbackSlot.captured.onConnectionStateChange( + device, + BluetoothGatt.GATT_SUCCESS, + BluetoothProfile.STATE_CONNECTED + ) + + assertEquals(device, manager.connectedDevice) + manager.cancelCurrentConnection() + advanceUntilIdle() + + assertNull(manager.connectedDevice) + verify(exactly = 1) { gattServer.cancelConnection(device) } + } } diff --git a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/BluetoothGattServerCallbackTest.kt b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/BluetoothGattServerCallbackTest.kt index ca30db26a..51a845ccd 100644 --- a/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/BluetoothGattServerCallbackTest.kt +++ b/bluetooth/src/test/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/BluetoothGattServerCallbackTest.kt @@ -64,7 +64,7 @@ class BluetoothGattServerCallbackTest { assertEquals(1, fakeEmitter.events.size) val event = fakeEmitter.events.single() as GattServerCallbackEvent.ServiceAdded assertEquals(BluetoothGatt.GATT_SUCCESS, event.status) - assertEquals(uuid, event.service?.uuid) + assertEquals(uuid, event.service.uuid) } @Test diff --git a/bluetooth/src/testFixtures/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/FakeGattServerManager.kt b/bluetooth/src/testFixtures/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/FakeGattServerManager.kt index 71210d89f..6db5c69d0 100644 --- a/bluetooth/src/testFixtures/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/FakeGattServerManager.kt +++ b/bluetooth/src/testFixtures/kotlin/uk/gov/onelogin/sharing/bluetooth/internal/peripheral/FakeGattServerManager.kt @@ -15,6 +15,7 @@ class FakeGattServerManager : GattServerManager { var openCalls = 0 var closeCalls = 0 + var disconnectCalls = 0 var notifySessionEnd: Boolean = true var sendMessageResult: Boolean = true val sentMessages = mutableListOf() @@ -23,6 +24,10 @@ class FakeGattServerManager : GattServerManager { openCalls++ } + override fun cancelCurrentConnection() { + disconnectCalls++ + } + override fun notifySessionEnd(serviceUuid: UUID): SessionEndStateQueued = if (notifySessionEnd) { _events.tryEmit(GattServerEvent.SessionEnd(SUCCESS))