diff --git a/CHANGELOG.md b/CHANGELOG.md index e892cbabd6..a366c7da8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ # 1.7.2 - In Progress +- [Feature] Fallback connection by flipper name - [FIX] Distinct fap items by id in paging sources # 1.7.1 diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/scanner/FlipperScanner.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/scanner/FlipperScanner.kt index fb93cb7de6..9b2caba497 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/scanner/FlipperScanner.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/scanner/FlipperScanner.kt @@ -14,5 +14,10 @@ interface FlipperScanner { /** * @return flipper by id */ - fun findFlipperById(deviceId: String): Flow + suspend fun findFlipperById(deviceId: String): Flow + + /** + * @return flipper by name + */ + fun findFlipperByName(deviceName: String): Flow } diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/Constants.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/Constants.kt index 20061d56a4..31df58f167 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/Constants.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/utils/Constants.kt @@ -2,7 +2,7 @@ package com.flipperdevices.bridge.api.utils import com.flipperdevices.core.data.SemVer import java.util.UUID -import java.util.concurrent.TimeUnit +import kotlin.time.Duration.Companion.seconds object Constants { const val DEVICENAME_PREFIX = "Flipper" @@ -76,8 +76,8 @@ object Constants { } object BLE { - private const val CONNECT_TIME_SEC = 3L - val CONNECT_TIME_MS = TimeUnit.MILLISECONDS.convert(CONNECT_TIME_SEC, TimeUnit.SECONDS) + val CONNECT_TIME = 3.seconds + val NEW_CONNECT_TIME = 15.seconds const val RECONNECT_COUNT = 1 const val RECONNECT_TIME_MS = 100L const val MAX_MTU = 512 diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperBleManagerImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperBleManagerImpl.kt index 807ac10384..5fcbf6c05e 100644 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperBleManagerImpl.kt +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperBleManagerImpl.kt @@ -98,11 +98,14 @@ class FlipperBleManagerImpl @Inject constructor( } override suspend fun connectToDevice(device: BluetoothDevice) { + info { "Schedule to connect: $device" } withLock(bleMutex, "connect") { + connectRequest?.cancelPendingConnection() + val connectRequestLocal = connect(device).retry( Constants.BLE.RECONNECT_COUNT, Constants.BLE.RECONNECT_TIME_MS.toInt() - ).useAutoConnect(true) + ) connectRequestLocal.enqueue() diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImpl.kt index 45329ec573..ae54430075 100644 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImpl.kt +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImpl.kt @@ -1,6 +1,7 @@ package com.flipperdevices.bridge.impl.scanner import android.Manifest +import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.pm.PackageManager @@ -16,6 +17,7 @@ import com.squareup.anvil.annotations.ContributesBinding import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge @@ -77,7 +79,7 @@ class FlipperScannerImpl @Inject constructor( } } - override fun findFlipperById(deviceId: String): Flow { + override suspend fun findFlipperById(deviceId: String): Flow { val bondedDevice = getAlreadyBondedDevices().firstOrNull { it.address == deviceId } @@ -88,6 +90,24 @@ class FlipperScannerImpl @Inject constructor( .map { DiscoveredBluetoothDevice(it) } } + @SuppressLint("MissingPermission") + override fun findFlipperByName( + deviceName: String, + ): Flow = flow { + getAlreadyBondedDevices().filter { + it.name == deviceName + }.forEach { + emit(it) + } + + scanner.scanFlow(provideSettings(), provideFilterForDefaultScan()) + .filter { it.device.name == deviceName } + .map { DiscoveredBluetoothDevice(it) } + .collect { + emit(it) + } + } + private fun getAlreadyBondedDevices(): List { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S && ActivityCompat.checkSelfPermission( diff --git a/components/bridge/impl/src/test/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImplTest.kt b/components/bridge/impl/src/test/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImplTest.kt index 2093e90544..262fa65917 100644 --- a/components/bridge/impl/src/test/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImplTest.kt +++ b/components/bridge/impl/src/test/java/com/flipperdevices/bridge/impl/scanner/FlipperScannerImplTest.kt @@ -182,6 +182,49 @@ class FlipperScannerImplTest { Assert.assertEquals(bluetoothDevice, foundDevice!!.device) } + @Test + fun `filter device by name filter`() = runTest { + val bluetoothDevice = mockk { + every { name } returns "Flipper Dumper" + every { address } returns "" + } + + val sr = ScanResult(bluetoothDevice, 0, 0, 0, 0, 0, 0, 0, null, 0L) + + every { scanner.scanFlow(any(), any()) } returns flowOf(sr) + every { bluetoothAdapter.bondedDevices } returns emptySet() + + val flipperDevices = flipperScanner.findFlipperByName("Flipper Dumper").first() + + val foundDevice = flipperDevices + Assert.assertNotNull(foundDevice) + Assert.assertEquals(bluetoothDevice, foundDevice.device) + } + + @Test + fun `filter device by name filter and return only this one`() = runTest { + val bluetoothDevice = mockk { + every { name } returns "Flipper Dumper" + every { address } returns "" + } + val wrongBluetoothDevice = mockk { + every { name } returns "Flipper Wrong" + every { address } returns "" + } + + val sr = ScanResult(bluetoothDevice, 0, 0, 0, 0, 0, 0, 0, null, 0L) + val srWrong = ScanResult(wrongBluetoothDevice, 0, 0, 0, 0, 0, 0, 0, null, 0L) + + every { scanner.scanFlow(any(), any()) } returns flowOf(sr, srWrong) + every { bluetoothAdapter.bondedDevices } returns emptySet() + + val flipperDevices = flipperScanner.findFlipperByName("Flipper Dumper").toList() + Assert.assertEquals(flipperDevices.size, 1) + val foundDevice = flipperDevices.first() + Assert.assertNotNull(foundDevice) + Assert.assertEquals(bluetoothDevice, foundDevice.device) + } + @Test fun `block device with incorrect name`() = runTest { val bluetoothDevice = mockk { diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperServiceApiImpl.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperServiceApiImpl.kt index b70c3b6500..db9112d8db 100644 --- a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperServiceApiImpl.kt +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperServiceApiImpl.kt @@ -3,8 +3,11 @@ package com.flipperdevices.bridge.service.impl import androidx.datastore.core.DataStore import com.flipperdevices.bridge.api.di.FlipperBleServiceGraph import com.flipperdevices.bridge.api.manager.FlipperBleManager +import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState import com.flipperdevices.bridge.service.api.FlipperServiceApi import com.flipperdevices.bridge.service.impl.delegate.FlipperSafeConnectWrapper +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionInformationApiWrapper +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo import com.flipperdevices.core.di.SingleIn import com.flipperdevices.core.di.provideDelegate import com.flipperdevices.core.ktx.jre.FlipperDispatchers @@ -17,9 +20,8 @@ import com.flipperdevices.core.preference.pb.PairSettings import com.flipperdevices.unhandledexception.api.UnhandledExceptionApi import com.squareup.anvil.annotations.ContributesBinding import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import java.util.concurrent.atomic.AtomicBoolean @@ -47,7 +49,12 @@ class FlipperServiceApiImpl @Inject constructor( private val mutex = Mutex() private var disconnectForced = false - override val connectionInformationApi = bleManager.connectionInformationApi + override val connectionInformationApi by lazy { + FlipperConnectionInformationApiWrapper( + flipperConnectionSource = bleManager.connectionInformationApi, + safeConnectWrapper = flipperSafeConnectWrapper + ) + } override val requestApi = bleManager.flipperRequestApi override val flipperInformationApi = bleManager.informationApi override val flipperVersionApi = bleManager.flipperVersionApi @@ -61,13 +68,34 @@ class FlipperServiceApiImpl @Inject constructor( var previousDeviceId: String? = null scope.launch(FlipperDispatchers.workStealingDispatcher) { - pairSettingsStore.data.map { it.deviceId }.collectLatest { deviceId -> + combine( + bleManager.connectionInformationApi + .getConnectionStateFlow(), + pairSettingsStore.data + ) { connectionState, pairSetting -> + (connectionState is ConnectionState.Disconnected) to SavedFlipperConnectionInfo.build( + pairSetting + ) + }.collect { (isDeviceDisconnected, connectionInfo) -> withLock(mutex, "connect") { - if (!unhandledExceptionApi.isBleConnectionForbiddenFlow().first() && - deviceId != previousDeviceId - ) { - previousDeviceId = deviceId - flipperSafeConnectWrapper.onActiveDeviceUpdate(deviceId) + if (unhandledExceptionApi.isBleConnectionForbiddenFlow().first()) { + return@withLock + } + + if (previousDeviceId != connectionInfo?.id) { // Reconnect + info { "Reconnect because device id changed" } + flipperSafeConnectWrapper.onActiveDeviceUpdate( + connectionInfo, + force = true + ) + previousDeviceId = connectionInfo?.id + } else if (isDeviceDisconnected && !disconnectForced && connectionInfo != null) { // Autoreconnect + info { "Reconnect because device is disconnected, but not forced" } + flipperSafeConnectWrapper.onActiveDeviceUpdate( + connectionInfo, + force = false + ) + previousDeviceId = connectionInfo?.id } } } @@ -82,23 +110,33 @@ class FlipperServiceApiImpl @Inject constructor( info { "Failed soft connect, because ble connection forbidden" } return@launchWithLock } - if (bleManager.isConnected() || flipperSafeConnectWrapper.isTryingConnected()) { + if (bleManager.isConnected() || flipperSafeConnectWrapper.isConnectingFlow().first()) { + info { "Skip soft connect because device already in connecting or connected stage" } return@launchWithLock } - val deviceId = pairSettingsStore.data.first().deviceId - flipperSafeConnectWrapper.onActiveDeviceUpdate(deviceId) + + val pairSetting = pairSettingsStore.data.first() + val connectionInfo = SavedFlipperConnectionInfo.build(pairSetting) + info { "Start soft connect to $connectionInfo" } + + flipperSafeConnectWrapper.onActiveDeviceUpdate(connectionInfo, force = true) } override suspend fun disconnect(isForce: Boolean) = withLock(mutex, "disconnect") { if (isForce) { disconnectForced = true } - flipperSafeConnectWrapper.onActiveDeviceUpdate(null) + flipperSafeConnectWrapper.onActiveDeviceUpdate(null, force = true) } override suspend fun reconnect() = withLock(mutex, "reconnect") { - val deviceId = pairSettingsStore.data.first().deviceId - flipperSafeConnectWrapper.onActiveDeviceUpdate(deviceId) + disconnectForced = false + val pairSetting = pairSettingsStore.data.first() + + flipperSafeConnectWrapper.onActiveDeviceUpdate( + SavedFlipperConnectionInfo.build(pairSetting), + force = true + ) } suspend fun close() = withLock(mutex, "close") { diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperSafeConnectWrapper.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperSafeConnectWrapper.kt index 8a96ece93a..f0fdde5bd0 100644 --- a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperSafeConnectWrapper.kt +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperSafeConnectWrapper.kt @@ -1,16 +1,23 @@ package com.flipperdevices.bridge.service.impl.delegate +import androidx.datastore.core.DataStore import com.flipperdevices.bridge.api.error.FlipperBleServiceError import com.flipperdevices.bridge.api.error.FlipperServiceErrorListener +import com.flipperdevices.bridge.service.impl.model.DeviceChangedMacException +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo +import com.flipperdevices.bridge.service.impl.utils.RemoveBondHelper import com.flipperdevices.core.di.provideDelegate import com.flipperdevices.core.ktx.jre.FlipperDispatchers import com.flipperdevices.core.ktx.jre.launchWithLock import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import com.flipperdevices.core.log.info +import com.flipperdevices.core.preference.pb.PairSettings import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex @@ -21,10 +28,14 @@ import javax.inject.Provider class FlipperSafeConnectWrapper @Inject constructor( scopeProvider: Provider, serviceErrorListenerProvider: Provider, - connectDelegateProvider: Provider + connectDelegateProvider: Provider, + dataStoreProvider: Provider>, + removeBondHelperProvider: Provider ) : LogTagProvider { override val TAG = "FlipperSafeConnectWrapper" + private val isConnectingMutableStateFlow = MutableStateFlow(false) + // It makes sure that we don't change the currentConnectingJob variable in different threads private val mutex = Mutex() private var currentConnectingJob: Job? = null @@ -32,43 +43,91 @@ class FlipperSafeConnectWrapper @Inject constructor( private val scope by scopeProvider private val serviceErrorListener by serviceErrorListenerProvider private val connectDelegate by connectDelegateProvider + private val dataStore by dataStoreProvider + private val removeBondHelper by removeBondHelperProvider + + fun isConnectingFlow() = isConnectingMutableStateFlow.asStateFlow() suspend fun onActiveDeviceUpdate( - deviceId: String? - ) = launchWithLock(mutex, scope, "onActiveDeviceUpdate") { - info { "Call cancel and join to current job" } - currentConnectingJob?.cancelAndJoin() - info { "Job canceled! Call connect again" } - currentConnectingJob = scope.launch(FlipperDispatchers.workStealingDispatcher) { - var errorOnDeviceUpdate: Throwable? - do { - errorOnDeviceUpdate = runCatching { - onActiveDeviceUpdateInternal(deviceId) - }.exceptionOrNull() - if (errorOnDeviceUpdate != null) { - error(errorOnDeviceUpdate) { "Unexpected error on activeDeviceUpdate" } + connectionInfo: SavedFlipperConnectionInfo?, + force: Boolean + ) { + launchWithLock(mutex, scope, "onActiveDeviceUpdate") { + if (force.not() && currentConnectingJob?.isActive == true) { + info { "onActiveDeviceUpdate called without force, so skip reinvalidate job" } + return@launchWithLock + } + info { "Call cancel and join to current job" } + currentConnectingJob?.cancelAndJoin() + info { "Job canceled! Call connect again" } + currentConnectingJob = scope.launch(FlipperDispatchers.workStealingDispatcher) { + var jobCompleted = false + do { + val deviceUpdateResult = runCatching { + onActiveDeviceUpdateInternal(connectionInfo) + } + val errorOnDeviceUpdate = deviceUpdateResult.exceptionOrNull() + if (errorOnDeviceUpdate != null) { + error(errorOnDeviceUpdate) { "Unexpected error on activeDeviceUpdate" } + } + if (deviceUpdateResult.getOrNull() == true) { + jobCompleted = true + } + } while (isActive && jobCompleted.not()) + if (jobCompleted) { + isConnectingMutableStateFlow.emit(false) } - } while (isActive && errorOnDeviceUpdate != null) + } } } - fun isTryingConnected() = currentConnectingJob?.isActive ?: false - - private suspend fun onActiveDeviceUpdateInternal(deviceId: String?) { - if (deviceId.isNullOrBlank()) { + private suspend fun onActiveDeviceUpdateInternal( + connectionInfo: SavedFlipperConnectionInfo? + ): Boolean { + if (connectionInfo == null || connectionInfo.id.isBlank()) { error { "Flipper id not found in storage" } connectDelegate.disconnect() - return + return true } + isConnectingMutableStateFlow.emit(true) try { - connectDelegate.reconnect(deviceId) + return connectDelegate.reconnect(connectionInfo) } catch (securityException: SecurityException) { serviceErrorListener.onError(FlipperBleServiceError.CONNECT_BLUETOOTH_PERMISSION) error(securityException) { "On initial connect to device" } + + return true } catch (bleDisabled: BluetoothDisabledException) { serviceErrorListener.onError(FlipperBleServiceError.CONNECT_BLUETOOTH_DISABLED) error(bleDisabled) { "On initial connect to device" } + + return true + } catch (changedMac: DeviceChangedMacException) { + info { "Mac changed from ${changedMac.oldMacAddress} to ${changedMac.newMacAddress}" } + + val newAddress = dataStore.updateData { data -> + if (data.deviceId == changedMac.oldMacAddress) { + return@updateData data.toBuilder() + .setDeviceId(changedMac.newMacAddress) + .build() + } else { + error { "Wrong device id for mac address change request" } + return@updateData data + } + } + + if (newAddress.deviceId == changedMac.newMacAddress && + changedMac.newMacAddress != changedMac.oldMacAddress + ) { + if (removeBondHelper.removeBond(changedMac.oldMacAddress).not()) { + error { "Failed remove bond for ${changedMac.oldMacAddress}" } + } else { + info { "Remove bond for ${changedMac.oldMacAddress}" } + } + } + + return false } } } diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperServiceConnectDelegate.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperServiceConnectDelegate.kt index 78ae5523b2..f6ffd50731 100644 --- a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperServiceConnectDelegate.kt +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperServiceConnectDelegate.kt @@ -4,15 +4,18 @@ import android.annotation.SuppressLint import android.bluetooth.BluetoothAdapter import android.content.Context import com.flipperdevices.bridge.api.manager.FlipperBleManager -import com.flipperdevices.bridge.api.scanner.FlipperScanner import com.flipperdevices.bridge.api.utils.Constants import com.flipperdevices.bridge.api.utils.PermissionHelper +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionByMac +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionByName +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionDelegate +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo import com.flipperdevices.core.di.provideDelegate import com.flipperdevices.core.ktx.jre.withLock +import com.flipperdevices.core.ktx.jre.withLockResult import com.flipperdevices.core.log.LogTagProvider import com.flipperdevices.core.log.error import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.flow.first import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withTimeout import no.nordicsemi.android.ble.exception.BluetoothDisabledException @@ -22,8 +25,9 @@ import javax.inject.Provider class FlipperServiceConnectDelegate @Inject constructor( bleManagerProvider: Provider, contextProvider: Provider, - scannerProvider: Provider, - adapterProvider: Provider + adapterProvider: Provider, + flipperConnectionByMac: Provider, + flipperConnectionByName: Provider ) : LogTagProvider { override val TAG = "FlipperServiceConnectDelegate" @@ -31,10 +35,16 @@ class FlipperServiceConnectDelegate @Inject constructor( private val bleManager by bleManagerProvider private val context by contextProvider - private val scanner by scannerProvider private val adapter by adapterProvider - suspend fun reconnect(deviceId: String) = withLock(mutex, "reconnect") { + private val connectionDelegates = listOf( + flipperConnectionByMac.get(), + flipperConnectionByName.get() + ) + + suspend fun reconnect( + connectionInfo: SavedFlipperConnectionInfo + ): Boolean = withLockResult(mutex, "reconnect") { // If we already connected to device, just ignore it disconnectInternal() @@ -57,7 +67,7 @@ class FlipperServiceConnectDelegate @Inject constructor( } // We try find device in manual mode and connect with it - findAndConnectToDeviceInternal(deviceId) + return@withLockResult findAndConnectToDeviceInternal(connectionInfo) } suspend fun disconnect() = withLock(mutex, "disconnect") { @@ -80,16 +90,13 @@ class FlipperServiceConnectDelegate @Inject constructor( // All rights must be obtained before calling this method @SuppressLint("MissingPermission") private suspend fun findAndConnectToDeviceInternal( - deviceId: String - ) { - var device = adapter.bondedDevices.find { it.address == deviceId } - - if (device == null) { - device = withTimeout(Constants.BLE.CONNECT_TIME_MS) { - scanner.findFlipperById(deviceId).first() - }.device + connectionInfo: SavedFlipperConnectionInfo + ): Boolean { + for (delegate in connectionDelegates) { + if (delegate.connect(connectionInfo)) { + return true + } } - - bleManager.connectToDevice(device) + return false } } diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByMac.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByMac.kt new file mode 100644 index 0000000000..f5d92b3ef7 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByMac.kt @@ -0,0 +1,58 @@ +package com.flipperdevices.bridge.service.impl.delegate.connection + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import com.flipperdevices.bridge.api.manager.FlipperBleManager +import com.flipperdevices.bridge.api.scanner.FlipperScanner +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo +import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Provider + +class FlipperConnectionByMac @Inject constructor( + bleManagerProvider: Provider, + scannerProvider: Provider, + adapterProvider: Provider +) : FlipperConnectionDelegate, LogTagProvider { + override val TAG = "FlipperConnectionByMac" + + private val bleManager by bleManagerProvider + private val scanner by scannerProvider + private val adapter by adapterProvider + + @SuppressLint("MissingPermission") + override suspend fun connect(connectionInfo: SavedFlipperConnectionInfo): Boolean { + info { "Start connection by $connectionInfo" } + var device = adapter.bondedDevices.find { it.address == connectionInfo.id } + + if (device == null) { + device = runCatching { + withTimeout(Constants.BLE.CONNECT_TIME) { + scanner.findFlipperById(connectionInfo.id).first() + }.device + }.getOrNull() + } + if (device == null) { + return false + } + + val connectWithTimeout = runCatching { + withTimeout(Constants.BLE.CONNECT_TIME) { + bleManager.connectToDevice(device) + } + } + val exception = connectWithTimeout.exceptionOrNull() + + if (exception != null) { + error(exception) { "Failed connect to device by MAC" } + return false + } + return true + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByName.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByName.kt new file mode 100644 index 0000000000..3337e59ef6 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionByName.kt @@ -0,0 +1,77 @@ +package com.flipperdevices.bridge.service.impl.delegate.connection + +import com.flipperdevices.bridge.api.manager.FlipperBleManager +import com.flipperdevices.bridge.api.scanner.FlipperScanner +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.bridge.service.impl.model.DeviceChangedMacException +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo +import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.timeout +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.withTimeout +import javax.inject.Inject +import javax.inject.Provider + +/** + * It's a fallback if the user's MAC address has changed, but the flipper is the same. + * This can happen in two cases: + * - Changing official firmware to custom firmware (and back again) + * - Updating official firmware after PR https://github.com/flipperdevices/flipperzero-firmware/pull/3723 + */ +class FlipperConnectionByName @Inject constructor( + bleManagerProvider: Provider, + scannerProvider: Provider +) : FlipperConnectionDelegate, LogTagProvider { + override val TAG = "FlipperConnectionByName" + + private val bleManager by bleManagerProvider + private val scanner by scannerProvider + + @OptIn(FlowPreview::class) + override suspend fun connect(connectionInfo: SavedFlipperConnectionInfo): Boolean { + info { "Start connection by $connectionInfo" } + if (connectionInfo.name == null) { + error { "Failed connect by ID and flipper name is unknown" } + return false + } + + val devices = scanner.findFlipperByName(connectionInfo.name).filter { + it.address != connectionInfo.id + }.timeout(Constants.BLE.CONNECT_TIME) + .catch { exception -> + if (exception !is TimeoutCancellationException) { + // Throw other exceptions. + throw exception + } + }.toList() + info { "Found: $devices" } + + for (device in devices) { + info { "Connect to ${device.address}..." } + val result = runCatching { + withTimeout(Constants.BLE.NEW_CONNECT_TIME) { + bleManager.connectToDevice(device.device) + } + } + if (result.isSuccess) { + info { "Connect to ${device.address} SUCCESS" } + + throw DeviceChangedMacException( + oldMacAddress = connectionInfo.id, + newMacAddress = device.address + ) + } else { + error(result.exceptionOrNull()) { "Connect to ${device.address} FAILED" } + } + } + + return false + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionDelegate.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionDelegate.kt new file mode 100644 index 0000000000..adfd122981 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionDelegate.kt @@ -0,0 +1,7 @@ +package com.flipperdevices.bridge.service.impl.delegate.connection + +import com.flipperdevices.bridge.service.impl.model.SavedFlipperConnectionInfo + +interface FlipperConnectionDelegate { + suspend fun connect(connectionInfo: SavedFlipperConnectionInfo): Boolean +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionInformationApiWrapper.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionInformationApiWrapper.kt new file mode 100644 index 0000000000..40d6000828 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/connection/FlipperConnectionInformationApiWrapper.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.bridge.service.impl.delegate.connection + +import com.flipperdevices.bridge.api.manager.delegates.FlipperConnectionInformationApi +import com.flipperdevices.bridge.api.manager.ktx.state.ConnectionState +import com.flipperdevices.bridge.service.impl.delegate.FlipperSafeConnectWrapper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +class FlipperConnectionInformationApiWrapper( + private val flipperConnectionSource: FlipperConnectionInformationApi, + private val safeConnectWrapper: FlipperSafeConnectWrapper +) : FlipperConnectionInformationApi { + + override fun isDeviceConnected() = flipperConnectionSource.isDeviceConnected() + + override fun getConnectionStateFlow(): Flow { + return combine( + flipperConnectionSource.getConnectionStateFlow(), + safeConnectWrapper.isConnectingFlow() + ) { state, isConnecting -> + when (state) { + is ConnectionState.Disconnected -> if (isConnecting) { + ConnectionState.Connecting + } else { + state + } + + else -> state + } + } + } + + override fun getConnectedDeviceName() = flipperConnectionSource.getConnectedDeviceName() +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperBleServiceComponentImpl.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperBleServiceComponentImpl.kt index 408c368afd..f826918e86 100644 --- a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperBleServiceComponentImpl.kt +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperBleServiceComponentImpl.kt @@ -12,6 +12,9 @@ import com.flipperdevices.bridge.service.impl.delegate.FlipperActionNotifierImpl import com.flipperdevices.bridge.service.impl.delegate.FlipperLagsDetectorImpl import com.flipperdevices.bridge.service.impl.delegate.FlipperSafeConnectWrapper import com.flipperdevices.bridge.service.impl.delegate.FlipperServiceConnectDelegate +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionByMac +import com.flipperdevices.bridge.service.impl.delegate.connection.FlipperConnectionByName +import com.flipperdevices.bridge.service.impl.utils.RemoveBondHelper import kotlinx.coroutines.CoroutineScope import javax.inject.Provider @@ -75,11 +78,32 @@ class FlipperBleServiceComponentImpl( ) } + private val flipperConnectionByMac by lazy { + FlipperConnectionByMac( + bleManagerProvider = { flipperBleManagerImpl }, + adapterProvider = { bluetoothAdapter }, + scannerProvider = { bluetoothScanner } + ) + } + private val flipperConnectionByName by lazy { + FlipperConnectionByName( + bleManagerProvider = { flipperBleManagerImpl }, + scannerProvider = { bluetoothScanner } + ) + } + private val flipperServiceConnectDelegate by lazy { FlipperServiceConnectDelegate( bleManagerProvider = { flipperBleManagerImpl }, contextProvider = { context }, - scannerProvider = { bluetoothScanner }, + adapterProvider = { bluetoothAdapter }, + flipperConnectionByMac = { flipperConnectionByMac }, + flipperConnectionByName = { flipperConnectionByName } + ) + } + + private val removeBondHelper by lazy { + RemoveBondHelper( adapterProvider = { bluetoothAdapter } ) } @@ -88,7 +112,9 @@ class FlipperBleServiceComponentImpl( FlipperSafeConnectWrapper( scopeProvider = { scope }, serviceErrorListenerProvider = { serviceErrorListener }, - connectDelegateProvider = { flipperServiceConnectDelegate } + connectDelegateProvider = { flipperServiceConnectDelegate }, + dataStoreProvider = { pairSettingsStore }, + removeBondHelperProvider = { removeBondHelper } ) } diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/DeviceChangedMacException.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/DeviceChangedMacException.kt new file mode 100644 index 0000000000..8acd29e918 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/DeviceChangedMacException.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.bridge.service.impl.model + +/** + * An Exception occurs when we realise that for some reason the device has changed its mac address + */ +data class DeviceChangedMacException( + val oldMacAddress: String, + val newMacAddress: String +) : RuntimeException() diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/SavedFlipperConnectionInfo.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/SavedFlipperConnectionInfo.kt new file mode 100644 index 0000000000..0928a789bb --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/model/SavedFlipperConnectionInfo.kt @@ -0,0 +1,29 @@ +package com.flipperdevices.bridge.service.impl.model + +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.core.preference.pb.PairSettings + +data class SavedFlipperConnectionInfo private constructor( + val id: String, + val name: String? +) { + companion object { + fun build( + pairSettings: PairSettings + ): SavedFlipperConnectionInfo? { + if (pairSettings.deviceId.isNullOrBlank()) { + return null + } + val flipperName = if (pairSettings.deviceName.startsWith(Constants.DEVICENAME_PREFIX)) { + pairSettings.deviceName + } else { + "${Constants.DEVICENAME_PREFIX} ${pairSettings.deviceName}" + } + + return SavedFlipperConnectionInfo( + id = pairSettings.deviceId, + name = flipperName + ) + } + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/RemoveBondHelper.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/RemoveBondHelper.kt new file mode 100644 index 0000000000..4e63338bd5 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/RemoveBondHelper.kt @@ -0,0 +1,48 @@ +package com.flipperdevices.bridge.service.impl.utils + +import android.annotation.SuppressLint +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import com.flipperdevices.core.di.provideDelegate +import com.flipperdevices.core.log.LogTagProvider +import com.flipperdevices.core.log.error +import com.flipperdevices.core.log.info +import javax.inject.Inject +import javax.inject.Provider + +class RemoveBondHelper @Inject constructor( + adapterProvider: Provider +) : LogTagProvider { + override val TAG = "RemoveBondHelper" + + private val adapter by adapterProvider + + @SuppressLint("MissingPermission") + fun removeBond(id: String): Boolean { + info { "Request remove bond for $id" } + val pairedDevices = adapter.bondedDevices.filter { it.address == id } + if (pairedDevices.isEmpty()) { + info { "Return false because no any paired device with id $id" } + return false // Not found any paired devices + } + info { "Found ${pairedDevices.size} paired devices with id $id" } + + var isSuccess = false + for (device in pairedDevices) { + val result = removeBond(device).onFailure { + error(it) { "Failed remove bond for device with id $id and device is $device" } + } + if (result.getOrNull() == true) { + info { "Remove bond successful for $device" } + isSuccess = true + } + } + return isSuccess // At lease one bond was deleted + } + + private fun removeBond(device: BluetoothDevice): Result = runCatching { + info { "Request remove bond for $device" } + val removeBond = device.javaClass.getMethod("removeBond") + return@runCatching removeBond.invoke(device) as Boolean + } +}