diff --git a/bluetooth/src/androidLibMain/kotlin/device/DeviceConnectionManager.kt b/bluetooth/src/androidLibMain/kotlin/device/DeviceConnectionManager.kt index 76e473976..dbe3e3133 100644 --- a/bluetooth/src/androidLibMain/kotlin/device/DeviceConnectionManager.kt +++ b/bluetooth/src/androidLibMain/kotlin/device/DeviceConnectionManager.kt @@ -35,6 +35,7 @@ import com.splendo.kaluga.bluetooth.Service import com.splendo.kaluga.bluetooth.UUID import com.splendo.kaluga.bluetooth.containsAnyOf import com.splendo.kaluga.bluetooth.uuidString +import com.splendo.kaluga.logging.info import com.splendo.kaluga.logging.warn import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -111,15 +112,18 @@ internal actual class DeviceConnectionManager( } override fun onConnectionStateChange(gatt: BluetoothGatt?, status: Int, newState: Int) { + warn(TAG) { "onConnectionStateChange($status): $lastKnownState -> $newState" } lastKnownState = newState launch(mainDispatcher) { when (newState) { BluetoothProfile.STATE_DISCONNECTED -> { + info(TAG) { "onConnectionStateChange($status): disconnect" } handleDisconnect { closeGatt() } } BluetoothProfile.STATE_CONNECTED -> { + info(TAG) { "onConnectionStateChange($status): Connect" } handleConnect() } } diff --git a/bluetooth/src/commonMain/kotlin/Bluetooth.kt b/bluetooth/src/commonMain/kotlin/Bluetooth.kt index 4cacee952..1ae481e9c 100644 --- a/bluetooth/src/commonMain/kotlin/Bluetooth.kt +++ b/bluetooth/src/commonMain/kotlin/Bluetooth.kt @@ -195,9 +195,12 @@ fun Flow.services(): Flow> { suspend fun Flow.connect() { state().transformLatest { deviceState -> when (deviceState) { - is Disconnected -> deviceState.startConnecting() is Connected -> emit(Unit) - is Connecting, is Reconnecting, is Disconnecting -> { } + is Disconnected.NotConnected -> deviceState.startConnecting() + is Disconnected.WaitingForBluetooth, + is Disconnecting, + is Connecting, + is Reconnecting -> { } } }.first() } diff --git a/bluetooth/src/commonMain/kotlin/device/DeviceConnectionManager.kt b/bluetooth/src/commonMain/kotlin/device/DeviceConnectionManager.kt index 613216989..f068bfbf2 100644 --- a/bluetooth/src/commonMain/kotlin/device/DeviceConnectionManager.kt +++ b/bluetooth/src/commonMain/kotlin/device/DeviceConnectionManager.kt @@ -113,6 +113,46 @@ abstract class BaseDeviceConnectionManager( } } + suspend fun handleBluetoothStateChange(isOn: Boolean) { + val clean = suspend { + currentAction = null + notifyingCharacteristics.clear() + } + + stateRepo.takeAndChangeState { state -> + if (!isOn) state.noBluetooth + else when (state) { + is DeviceState.Disconnected.WaitingForBluetooth -> state.onBluetoothAvailable().also { + if (it == state.didDisconnect) { + clean() + } + } + else -> state.remain() + // is DeviceState.Reconnecting -> { + // state.retry().also { + // if (it == state.didDisconnect) { + // clean() + // } + // } + // } + // is DeviceState.Connected -> when (connectionSettings.reconnectionSettings) { + // is ConnectionSettings.ReconnectionSettings.Always, + // is ConnectionSettings.ReconnectionSettings.Limited -> state.reconnect + // is ConnectionSettings.ReconnectionSettings.Never -> { + // clean() + // state.didDisconnect + // } + // } + // is DeviceState.Disconnected -> state.remain() + // is DeviceState.Connecting, + // is DeviceState.Disconnecting -> { + // clean() + // state.didDisconnect + // } + } + } + } + suspend fun handleScanCompleted(services: List) { stateRepo.takeAndChangeState { state -> when (state) { @@ -178,6 +218,9 @@ abstract class BaseDeviceConnectionManager( handleCurrentActionCompleted(succeeded) } } + + private suspend fun handleNoBluetooth() { + } } internal expect class DeviceConnectionManager : BaseDeviceConnectionManager diff --git a/bluetooth/src/commonMain/kotlin/device/DeviceState.kt b/bluetooth/src/commonMain/kotlin/device/DeviceState.kt index c7896f2a1..b677c3319 100644 --- a/bluetooth/src/commonMain/kotlin/device/DeviceState.kt +++ b/bluetooth/src/commonMain/kotlin/device/DeviceState.kt @@ -169,6 +169,7 @@ sealed class DeviceState( connectionManager.disconnect() } } + data class Connecting constructor( override val deviceInfo: DeviceInfoImpl, override val connectionManager: BaseDeviceConnectionManager @@ -252,25 +253,47 @@ sealed class DeviceState( } } - data class Disconnected constructor( + sealed class Disconnected constructor( override val deviceInfo: DeviceInfoImpl, override val connectionManager: BaseDeviceConnectionManager ) : DeviceState(deviceInfo, connectionManager) { - fun startConnecting() = connectionManager.stateRepo.launchTakeAndChangeState( - coroutineContext, - remainIfStateNot = Disconnected::class - ) { deviceState -> - connect(deviceState) + data class WaitingForBluetooth constructor( + override val deviceInfo: DeviceInfoImpl, + override val connectionManager: BaseDeviceConnectionManager, + val services: List? + ) : Disconnected(deviceInfo, connectionManager) { + + fun onBluetoothAvailable(): suspend () -> DeviceState = suspend { + val shouldReconnect = connectionManager.connectionSettings.reconnectionSettings != ConnectionSettings.ReconnectionSettings.Never + if (shouldReconnect && deviceInfo.advertisementData.isConnectible) { + Reconnecting(0, services, deviceInfo, connectionManager) + } else { + NotConnected(deviceInfo, connectionManager) + } + } } - fun connect(deviceState: DeviceState): suspend () -> DeviceState = - if (deviceInfo.advertisementData.isConnectible) - suspend { - Connecting(deviceInfo, connectionManager) - } - else - deviceState.remain() + data class NotConnected constructor( + override val deviceInfo: DeviceInfoImpl, + override val connectionManager: BaseDeviceConnectionManager + ) : Disconnected(deviceInfo, connectionManager) { + + fun startConnecting() = connectionManager.stateRepo.launchTakeAndChangeState( + coroutineContext, + remainIfStateNot = NotConnected::class + ) { deviceState -> + connect(deviceState) + } + + fun connect(deviceState: DeviceState): suspend () -> DeviceState = + if (deviceInfo.advertisementData.isConnectible) + suspend { + Connecting(deviceInfo, connectionManager) + } + else + deviceState.remain() + } } data class Disconnecting constructor( @@ -304,13 +327,29 @@ sealed class DeviceState( is Connecting -> copy(deviceInfo = newDeviceInfo) is Reconnecting -> copy(deviceInfo = newDeviceInfo) is Disconnecting -> copy(deviceInfo = newDeviceInfo) - is Disconnected -> copy(deviceInfo = newDeviceInfo) + is Disconnected.WaitingForBluetooth -> copy(deviceInfo = newDeviceInfo) + is Disconnected.NotConnected -> copy(deviceInfo = newDeviceInfo) } } } + fun bluetoothSwitchedOff(): suspend () -> DeviceState = + when (this) { + is Connecting, + is Reconnecting, + is Connected -> noBluetooth + is Disconnecting -> didDisconnect + is Disconnected.NotConnected, + is Disconnected.WaitingForBluetooth -> remain() + } + + internal val noBluetooth = suspend { + // All services, characteristics and descriptors become invalidated after it disconnects + Disconnected.WaitingForBluetooth(deviceInfo, connectionManager, null) + } + internal val didDisconnect = suspend { - Disconnected(deviceInfo, connectionManager) + Disconnected.NotConnected(deviceInfo, connectionManager) } internal val disconnecting = suspend { @@ -327,7 +366,7 @@ class Device constructor( coroutineContext = coroutineContext, initialState = { val deviceConnectionManager = connectionBuilder.create(connectionSettings, initialDeviceInfo.deviceWrapper, it) - DeviceState.Disconnected(initialDeviceInfo, deviceConnectionManager) + DeviceState.Disconnected.NotConnected(initialDeviceInfo, deviceConnectionManager) } ) { val identifier: Identifier diff --git a/bluetooth/src/commonMain/kotlin/scanner/Scanner.kt b/bluetooth/src/commonMain/kotlin/scanner/Scanner.kt index caed1d89d..4f525f673 100644 --- a/bluetooth/src/commonMain/kotlin/scanner/Scanner.kt +++ b/bluetooth/src/commonMain/kotlin/scanner/Scanner.kt @@ -22,6 +22,7 @@ import com.splendo.kaluga.base.flow.filterOnlyImportant import com.splendo.kaluga.bluetooth.BluetoothMonitor import com.splendo.kaluga.bluetooth.UUID import com.splendo.kaluga.bluetooth.device.AdvertisementData +import com.splendo.kaluga.bluetooth.device.BaseDeviceConnectionManager import com.splendo.kaluga.bluetooth.device.ConnectionSettings import com.splendo.kaluga.bluetooth.device.Device import com.splendo.kaluga.bluetooth.device.Identifier @@ -148,10 +149,14 @@ abstract class BaseScanner constructor( fun bluetoothEnabled() = stateRepo.launchTakeAndChangeState(remainIfStateNot = Disabled::class) { it.enable + }.also { + notifyPeripherals { handleBluetoothStateChange(isOn = true) } } fun bluetoothDisabled() = stateRepo.launchTakeAndChangeState(remainIfStateNot = Enabled::class) { it.disable + }.also { + notifyPeripherals { handleBluetoothStateChange(isOn = false) } } internal fun handleDeviceDiscovered(identifier: Identifier, rssi: Int, advertisementData: AdvertisementData, deviceCreator: () -> Device) = @@ -173,6 +178,15 @@ abstract class BaseScanner constructor( else -> bluetoothDisabled() } } + + private fun notifyPeripherals(block: suspend BaseDeviceConnectionManager.() -> Unit) = suspend { + stateRepo.launchUseState { scannerState -> + if (scannerState is ScanningState.Initialized.Enabled) + scannerState.discovered.devices.forEach { device -> + block(device.peekState().connectionManager) + } + } + } } expect class Scanner : BaseScanner diff --git a/bluetooth/src/commonTest/kotlin/BluetoothCharacteristicNotificationTest.kt b/bluetooth/src/commonTest/kotlin/BluetoothCharacteristicNotificationTest.kt index afddde1d2..0b15e7499 100644 --- a/bluetooth/src/commonTest/kotlin/BluetoothCharacteristicNotificationTest.kt +++ b/bluetooth/src/commonTest/kotlin/BluetoothCharacteristicNotificationTest.kt @@ -103,7 +103,7 @@ class BluetoothCharacteristicNotificationTest : BluetoothFlowTest() device.takeAndChangeState { deviceState -> println("state: $deviceState") when (deviceState) { - is DeviceState.Disconnected -> deviceState.connect(deviceState) + is DeviceState.Disconnected.NotConnected -> deviceState.connect(deviceState) else -> fail("$deviceState is not expected") } } diff --git a/bluetooth/src/commonTest/kotlin/device/DeviceTest.kt b/bluetooth/src/commonTest/kotlin/device/DeviceTest.kt index 945b27077..0f1412a29 100644 --- a/bluetooth/src/commonTest/kotlin/device/DeviceTest.kt +++ b/bluetooth/src/commonTest/kotlin/device/DeviceTest.kt @@ -101,7 +101,7 @@ class DeviceTest : BluetoothFlowTest() { action { deviceStateRepo.takeAndChangeState { deviceState -> when (deviceState) { - is DeviceState.Disconnected -> { + is DeviceState.Disconnected.NotConnected -> { deviceState.connect(deviceState).also { assertEquals(deviceState.remain(), it) } @@ -369,7 +369,7 @@ class DeviceTest : BluetoothFlowTest() { deviceStateRepo.takeAndChangeState { deviceState -> println("state: $deviceState") when (deviceState) { - is DeviceState.Disconnected -> deviceState.connect(deviceState) + is DeviceState.Disconnected.NotConnected -> deviceState.connect(deviceState) else -> fail("$deviceState is not expected") } } diff --git a/bluetooth/src/iosMain/kotlin/scanner/Scanner.kt b/bluetooth/src/iosMain/kotlin/scanner/Scanner.kt index bb29918ad..a14b04544 100644 --- a/bluetooth/src/iosMain/kotlin/scanner/Scanner.kt +++ b/bluetooth/src/iosMain/kotlin/scanner/Scanner.kt @@ -81,15 +81,28 @@ actual class Scanner internal constructor( private class PoweredOnCBCentralManagerDelegate(private val scanner: Scanner, private val isEnabledCompleted: EmptyCompletableDeferred) : NSObject(), CBCentralManagerDelegateProtocol { override fun centralManagerDidUpdateState(central: CBCentralManager) = mainContinuation { - if (central.state == CBCentralManagerStatePoweredOn) { + val isEnabled = central.state == CBCentralManagerStatePoweredOn + if (isEnabled) { isEnabledCompleted.complete() } + notifyPeripherals { + handleBluetoothStateChange(isOn = isEnabled) + } }.invoke() override fun centralManager(central: CBCentralManager, didDiscoverPeripheral: CBPeripheral, advertisementData: Map, RSSI: NSNumber) { scanner.discoverPeripheral(central, didDiscoverPeripheral, advertisementData.typedMap(), RSSI.intValue) } + fun notifyPeripherals(block: suspend BaseDeviceConnectionManager.() -> Unit) = mainContinuation { + scanner.stateRepo.launchUseState { scannerState -> + if (scannerState is ScanningState.Initialized.Enabled) + scannerState.discovered.devices.forEach { device -> + block(device.peekState().connectionManager) + } + } + }.invoke() + fun handlePeripheral(didConnectPeripheral: CBPeripheral, block: suspend BaseDeviceConnectionManager.() -> Unit) = mainContinuation { scanner.stateRepo.launchUseState { scannerState -> if (scannerState is ScanningState.Initialized.Enabled)