diff --git a/.github/workflows/internal.yml b/.github/workflows/internal.yml index 4619ec42c4..872082c8a4 100644 --- a/.github/workflows/internal.yml +++ b/.github/workflows/internal.yml @@ -6,7 +6,7 @@ on: - 'dev' env: - VERSION: 0.2 + VERSION: 0.3 jobs: build_internal_release: diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt index c80de0577b..d904b5e2e6 100644 --- a/buildSrc/src/main/java/Dependencies.kt +++ b/buildSrc/src/main/java/Dependencies.kt @@ -63,6 +63,10 @@ object Libs { "androidx.lifecycle:lifecycle-viewmodel-ktx:${Versions.ANDROID_LIFECYCLE}" const val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:${Versions.ANDROID_LIFECYCLE}" + const val LIFECYCLE_SERVICE = + "androidx.lifecycle:lifecycle-service:${Versions.ANDROID_LIFECYCLE}" + const val LIFECYCLE_KAPT = + "androidx.lifecycle:lifecycle-compiler:${Versions.ANDROID_LIFECYCLE}" const val ANNOTATIONS = "androidx.annotation:annotation:${Versions.ANDROID_ANNOTATIONS}" const val APPCOMPAT = "androidx.appcompat:appcompat:${Versions.ANDROIDX_APPCOMPAT}" diff --git a/buildSrc/src/main/java/com/flipperdevices/gradle/AppExtensionConfiguration.kt b/buildSrc/src/main/java/com/flipperdevices/gradle/AppExtensionConfiguration.kt index 4c16049890..fb3d0b7377 100644 --- a/buildSrc/src/main/java/com/flipperdevices/gradle/AppExtensionConfiguration.kt +++ b/buildSrc/src/main/java/com/flipperdevices/gradle/AppExtensionConfiguration.kt @@ -56,4 +56,4 @@ private fun AppExtension.configureBuildFeatures() { private fun AppExtension.configureCompileOptions() { compileOptions.sourceCompatibility = JavaVersion.VERSION_1_8 compileOptions.targetCompatibility = JavaVersion.VERSION_1_8 -} \ No newline at end of file +} diff --git a/components/bottombar/src/main/java/com/flipperdevices/bottombar/navigate/ScreenTabProviderImpl.kt b/components/bottombar/src/main/java/com/flipperdevices/bottombar/navigate/ScreenTabProviderImpl.kt index e6970cd0a9..dff053550e 100644 --- a/components/bottombar/src/main/java/com/flipperdevices/bottombar/navigate/ScreenTabProviderImpl.kt +++ b/components/bottombar/src/main/java/com/flipperdevices/bottombar/navigate/ScreenTabProviderImpl.kt @@ -5,7 +5,6 @@ import com.flipperdevices.bottombar.model.FlipperBottomTab import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.navigation.screen.InfoScreenProvider import com.flipperdevices.filemanager.api.navigation.FileManagerScreenProvider -import com.flipperdevices.pair.api.PairComponentApi import com.github.terrakok.cicerone.Screen import com.github.terrakok.cicerone.androidx.FragmentScreen import com.squareup.anvil.annotations.ContributesBinding @@ -14,17 +13,12 @@ import javax.inject.Inject @ContributesBinding(AppGraph::class) class ScreenTabProviderImpl @Inject constructor( private val infoScreenProvider: InfoScreenProvider, - private val fileManagerScreenProvider: FileManagerScreenProvider, - private val pairComponentApi: PairComponentApi + private val fileManagerScreenProvider: FileManagerScreenProvider ) : ScreenTabProvider { override fun getScreen(tab: FlipperBottomTab): Screen { return when (tab) { - FlipperBottomTab.DEVICE -> infoScreenProvider.deviceInformationScreen( - pairComponentApi.getPairedDevice() - ) - FlipperBottomTab.STORAGE -> fileManagerScreenProvider.fileManager( - pairComponentApi.getPairedDevice() - ) + FlipperBottomTab.DEVICE -> infoScreenProvider.deviceInformationScreen() + FlipperBottomTab.STORAGE -> fileManagerScreenProvider.fileManager("/") else -> FragmentScreen { TestFragment() } } } diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/device/FlipperDeviceApi.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/device/FlipperDeviceApi.kt deleted file mode 100644 index 4713cc86d8..0000000000 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/device/FlipperDeviceApi.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.flipperdevices.bridge.api.device - -import com.flipperdevices.bridge.api.manager.FlipperBleManager - -/** - * Provide API to Flipper Device - * For get instance of this object, use {@link FlipperPairApi#connect} - */ -interface FlipperDeviceApi { - val address: String - fun getBleManager(): FlipperBleManager -} diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/di/FlipperBleComponentInterface.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/di/FlipperBleComponentInterface.kt index 9b3bd1fbc3..056ba27e73 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/di/FlipperBleComponentInterface.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/di/FlipperBleComponentInterface.kt @@ -1,9 +1,9 @@ package com.flipperdevices.bridge.api.di -import com.flipperdevices.bridge.api.pair.FlipperPairApi +import android.bluetooth.BluetoothAdapter import com.flipperdevices.bridge.api.scanner.FlipperScanner interface FlipperBleComponentInterface { val flipperScanner: FlipperScanner - val flipperPairApi: FlipperPairApi + val bluetoothAdapter: BluetoothAdapter } diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperBleManager.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperBleManager.kt index 90ae34dce1..34c0cdd155 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperBleManager.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperBleManager.kt @@ -1,15 +1,33 @@ package com.flipperdevices.bridge.api.manager import android.bluetooth.BluetoothDevice -import com.flipperdevices.bridge.api.model.FlipperGATTInformation -import kotlinx.coroutines.flow.StateFlow -import no.nordicsemi.android.ble.ktx.state.ConnectionState +import com.flipperdevices.bridge.api.manager.delegates.FlipperConnectionInformationApi +import com.flipperdevices.bridge.api.manager.service.FlipperInformationApi +import com.flipperdevices.bridge.api.manager.service.FlipperSerialApi -interface FlipperBleManager : FlipperSerialApi { - val isDeviceConnected: Boolean +interface FlipperBleManager { + // Manager delegates + val connectionInformationApi: FlipperConnectionInformationApi + + // This section provide access to device apis + val informationApi: FlipperInformationApi val flipperRequestApi: FlipperRequestApi - fun getInformationStateFlow(): StateFlow - fun getConnectionStateFlow(): StateFlow - fun connectToDevice(device: BluetoothDevice) - fun disconnectDevice() + val serialApi: FlipperSerialApi + + /** + * Connect to device {@param device} + * Await while disconnect process is not finish + */ + suspend fun connectToDevice(device: BluetoothDevice) + + /** + * Disconnect from current device + * Await while disconnect process is not finish + */ + suspend fun disconnectDevice() + + /** + * Close manager, unregister receivers + */ + fun close() } diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/delegates/FlipperConnectionInformationApi.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/delegates/FlipperConnectionInformationApi.kt new file mode 100644 index 0000000000..9c720d24e1 --- /dev/null +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/delegates/FlipperConnectionInformationApi.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.bridge.api.manager.delegates + +import kotlinx.coroutines.flow.StateFlow +import no.nordicsemi.android.ble.ktx.state.ConnectionState + +interface FlipperConnectionInformationApi { + fun isDeviceConnected(): Boolean + fun getConnectionStateFlow(): StateFlow +} diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperInformationApi.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperInformationApi.kt new file mode 100644 index 0000000000..345fb5ec05 --- /dev/null +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperInformationApi.kt @@ -0,0 +1,8 @@ +package com.flipperdevices.bridge.api.manager.service + +import com.flipperdevices.bridge.api.model.FlipperGATTInformation +import kotlinx.coroutines.flow.StateFlow + +interface FlipperInformationApi { + fun getInformationFlow(): StateFlow +} diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperSerialApi.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperSerialApi.kt similarity index 73% rename from components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperSerialApi.kt rename to components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperSerialApi.kt index bbeaa035fa..aa6912c96f 100644 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/FlipperSerialApi.kt +++ b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/manager/service/FlipperSerialApi.kt @@ -1,4 +1,4 @@ -package com.flipperdevices.bridge.api.manager +package com.flipperdevices.bridge.api.manager.service import kotlinx.coroutines.flow.Flow diff --git a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/pair/FlipperPairApi.kt b/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/pair/FlipperPairApi.kt deleted file mode 100644 index b94a45b0dd..0000000000 --- a/components/bridge/api/src/main/java/com/flipperdevices/bridge/api/pair/FlipperPairApi.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.flipperdevices.bridge.api.pair - -import android.bluetooth.BluetoothDevice -import android.content.Context -import com.flipperdevices.bridge.api.device.FlipperDeviceApi -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.TimeoutCancellationException -import no.nordicsemi.android.ble.exception.BluetoothDisabledException - -interface FlipperPairApi { - fun getFlipperApi( - context: Context, - deviceId: String - ): FlipperDeviceApi - - @Throws( - SecurityException::class, - BluetoothDisabledException::class, - TimeoutCancellationException::class, - IllegalArgumentException::class - ) - @ExperimentalCoroutinesApi - suspend fun connect( - context: Context, - flipperDeviceApi: FlipperDeviceApi - ) - - /** - * Main difference in args - */ - fun scheduleConnect( - flipperDeviceApi: FlipperDeviceApi, - device: BluetoothDevice - ) -} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/device/FlipperDeviceApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/device/FlipperDeviceApiImpl.kt deleted file mode 100644 index 4d4b55671e..0000000000 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/device/FlipperDeviceApiImpl.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.flipperdevices.bridge.impl.device - -import com.flipperdevices.bridge.api.device.FlipperDeviceApi -import com.flipperdevices.bridge.impl.manager.FlipperBleManagerImpl - -class FlipperDeviceApiImpl( - private val bleManagerImpl: FlipperBleManagerImpl, - override val address: String -) : FlipperDeviceApi { - override fun getBleManager(): FlipperBleManagerImpl { - return bleManagerImpl - } -} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/di/FlipperBleModule.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/di/FlipperBleModule.kt index 13a015dcdb..a7eaff8bc1 100644 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/di/FlipperBleModule.kt +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/di/FlipperBleModule.kt @@ -1,8 +1,6 @@ package com.flipperdevices.bridge.impl.di -import com.flipperdevices.bridge.api.pair.FlipperPairApi import com.flipperdevices.bridge.api.scanner.FlipperScanner -import com.flipperdevices.bridge.impl.pair.FlipperPairApiImpl import com.flipperdevices.bridge.impl.scanner.FlipperScannerImpl import dagger.Binds import dagger.Module @@ -13,8 +11,4 @@ abstract class FlipperBleModule { @Binds @Singleton abstract fun provideFlipperScanner(flipperScanner: FlipperScannerImpl): FlipperScanner - - @Binds - @Singleton - abstract fun provideFlipperPairApi(flipperPairApi: FlipperPairApiImpl): FlipperPairApi } 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 51500ea25f..cd0be94ee9 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 @@ -2,65 +2,55 @@ package com.flipperdevices.bridge.impl.manager import android.bluetooth.BluetoothDevice import android.bluetooth.BluetoothGatt -import android.bluetooth.BluetoothGattCharacteristic import android.content.Context import com.flipperdevices.bridge.api.manager.FlipperBleManager -import com.flipperdevices.bridge.api.manager.FlipperRequestApi -import com.flipperdevices.bridge.api.model.FlipperGATTInformation import com.flipperdevices.bridge.api.utils.Constants -import java.util.UUID +import com.flipperdevices.bridge.impl.manager.delegates.FlipperConnectionInformationApiImpl +import com.flipperdevices.bridge.impl.manager.service.FlipperInformationApiImpl +import com.flipperdevices.bridge.impl.manager.service.FlipperSerialApiImpl +import com.flipperdevices.core.utils.newSingleThreadExecutor import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import no.nordicsemi.android.ble.BleManager -import no.nordicsemi.android.ble.ktx.state.ConnectionState -import no.nordicsemi.android.ble.ktx.stateAsFlow +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.withContext import timber.log.Timber -class FlipperBleManagerImpl( +@Suppress("BlockingMethodInNonBlockingContext") +class FlipperBleManagerImpl constructor( context: Context, private val scope: CoroutineScope -) : BleManager(context), FlipperBleManager { - private val informationState = MutableStateFlow(FlipperGATTInformation()) - private val receiveBytesFlow = MutableSharedFlow() - private val infoCharacteristics = mutableMapOf() - private var serialTxCharacteristic: BluetoothGattCharacteristic? = null - private var serialRxCharacteristic: BluetoothGattCharacteristic? = null - - override val flipperRequestApi: FlipperRequestApi = FlipperRequestApiImpl(this, scope) - override val isDeviceConnected = super.isConnected() - override fun getInformationStateFlow(): StateFlow = informationState - override fun getConnectionStateFlow(): StateFlow = stateAsFlow() - override fun disconnectDevice() = disconnect().enqueue() - override fun receiveBytesFlow(): Flow { - return receiveBytesFlow +) : UnsafeBleManager(context), FlipperBleManager { + private val bleDispatcher = newSingleThreadExecutor("FlipperBleManagerImpl") + .asCoroutineDispatcher() + + // Gatt Delegates + override val informationApi = FlipperInformationApiImpl() + override val serialApi = FlipperSerialApiImpl(scope) + override val flipperRequestApi = FlipperRequestApiImpl(serialApi, scope) + + // Manager delegates + override val connectionInformationApi = FlipperConnectionInformationApiImpl(this) + + init { + setConnectionObserver(ConnectionObserverLogger()) + Timber.i("FlipperBleManagerImpl: ${this.hashCode()}") } - override fun sendBytes(data: ByteArray) { - writeCharacteristic(serialTxCharacteristic, data).enqueue() + override suspend fun disconnectDevice() = withContext(bleDispatcher) { + disconnect().await() } - override fun connectToDevice(device: BluetoothDevice) { + override suspend fun connectToDevice(device: BluetoothDevice) = withContext(bleDispatcher) { connect(device).retry( Constants.BLE.RECONNECT_COUNT, Constants.BLE.RECONNECT_TIME_MS.toInt() ).useAutoConnect(true) - .enqueue() + .await() } override fun log(priority: Int, message: String) { Timber.d(message) } - init { - setConnectionObserver(ConnectionObserverLogger()) - } - override fun getGattCallback(): BleManagerGattCallback = FlipperBleManagerGattCallback() @@ -75,92 +65,37 @@ class FlipperBleManagerImpl( } override fun onDeviceReady() { - registerToInformationGATT() - registerToSerialGATT() + // Set up large MTU + // Also does not work with small MTU because of a bug in Flipper Zero firmware + requestMtu(Constants.BLE.MTU).enqueue() + + informationApi.initialize(this@FlipperBleManagerImpl) + serialApi.initialize(this@FlipperBleManagerImpl) } override fun isRequiredServiceSupported(gatt: BluetoothGatt): Boolean { gatt.services.forEach { service -> service.characteristics.forEach { - Timber.i("Characteristic for service ${service.uuid}: ${it.uuid}") + Timber.d("Characteristic for service ${service.uuid}: ${it.uuid}") } } - val informationService = - gatt.getService(Constants.BLEInformationService.SERVICE_UUID) - - informationService?.characteristics?.forEach { - infoCharacteristics[it.uuid] = it - } - - val serialService = + serialApi.onServiceReceived( gatt.getService(Constants.BLESerialService.SERVICE_UUID) - - serialTxCharacteristic = serialService?.getCharacteristic(Constants.BLESerialService.TX) - serialRxCharacteristic = serialService?.getCharacteristic(Constants.BLESerialService.RX) - - val genericService = gatt.getService(Constants.GenericService.SERVICE_UUID) - - genericService?.characteristics?.find { - it.uuid.equals(Constants.GenericService.DEVICE_NAME) - }?.let { - infoCharacteristics[Constants.GenericService.DEVICE_NAME] = it - } + ) + informationApi.onServiceReceived( + gatt.getService(Constants.BLEInformationService.SERVICE_UUID) + ) + informationApi.onServiceReceived( + gatt.getService(Constants.GenericService.SERVICE_UUID) + ) return true } override fun onServicesInvalidated() { - // TODO reset state - } - } - - @DelicateCoroutinesApi // TODO replace it - private fun registerToSerialGATT() { - setNotificationCallback(serialRxCharacteristic).with { _, data -> - Timber.i("Receive serial data") - val bytes = data.value ?: return@with - scope.launch { - receiveBytesFlow.emit(bytes) - } + informationApi.reset() + serialApi.reset() } - requestMtu(Constants.BLE.MTU).enqueue() - enableNotifications(serialRxCharacteristic).enqueue() - enableIndications(serialRxCharacteristic).enqueue() - } - - private fun registerToInformationGATT() { - readCharacteristic( - infoCharacteristics[Constants.BLEInformationService.MANUFACTURER] - ).with { _, data -> - val content = data.value ?: return@with - informationState.update { - it.copy(manufacturerName = String(content)) - } - }.enqueue() - readCharacteristic( - infoCharacteristics[Constants.GenericService.DEVICE_NAME] - ).with { _, data -> - val content = data.value ?: return@with - informationState.update { - it.copy(deviceName = String(content)) - } - }.enqueue() - readCharacteristic( - infoCharacteristics[Constants.BLEInformationService.HARDWARE_VERSION] - ).with { _, data -> - val content = data.value ?: return@with - informationState.update { - it.copy(hardwareRevision = String(content)) - } - }.enqueue() - readCharacteristic( - infoCharacteristics[Constants.BLEInformationService.SOFTWARE_VERSION] - ).with { _, data -> - val content = data.value ?: return@with - informationState.update { - it.copy(softwareVersion = String(content)) - } - }.enqueue() } } diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperRequestApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperRequestApiImpl.kt index 074f65896b..4f241d48c2 100644 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperRequestApiImpl.kt +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/FlipperRequestApiImpl.kt @@ -3,9 +3,10 @@ package com.flipperdevices.bridge.impl.manager import android.util.SparseArray import androidx.core.util.set import com.flipperdevices.bridge.api.manager.FlipperRequestApi -import com.flipperdevices.bridge.api.manager.FlipperSerialApi +import com.flipperdevices.bridge.api.manager.service.FlipperSerialApi import com.flipperdevices.protobuf.Flipper import com.flipperdevices.protobuf.copy +import java.io.ByteArrayOutputStream import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ObsoleteCoroutinesApi @@ -17,7 +18,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import timber.log.Timber -import java.io.ByteArrayOutputStream private typealias OnReceiveResponse = (Flipper.Main) -> Unit diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/UnsafeBleManager.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/UnsafeBleManager.kt new file mode 100644 index 0000000000..d6877cfc23 --- /dev/null +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/UnsafeBleManager.kt @@ -0,0 +1,28 @@ +package com.flipperdevices.bridge.impl.manager + +import android.bluetooth.BluetoothGattCharacteristic +import android.content.Context +import no.nordicsemi.android.ble.BleManager + +/** + * BleManager from nordic library use protected method + * So we can't call it outside BleManager + * And the BleManager becomes very big + * This wrapper allows you to call protected methods to delegates + */ +abstract class UnsafeBleManager(context: Context) : BleManager(context) { + fun readCharacteristicUnsafe(characteristic: BluetoothGattCharacteristic?) = + readCharacteristic(characteristic) + + fun writeCharacteristicUnsafe(characteristic: BluetoothGattCharacteristic?, data: ByteArray) = + writeCharacteristic(characteristic, data) + + fun setNotificationCallbackUnsafe(characteristic: BluetoothGattCharacteristic?) = + setNotificationCallback(characteristic) + + fun enableNotificationsUnsafe(characteristic: BluetoothGattCharacteristic?) = + enableNotifications(characteristic) + + fun enableIndicationsUnsafe(characteristic: BluetoothGattCharacteristic?) = + enableIndications(characteristic) +} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/delegates/FlipperConnectionInformationApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/delegates/FlipperConnectionInformationApiImpl.kt new file mode 100644 index 0000000000..4f565a7102 --- /dev/null +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/delegates/FlipperConnectionInformationApiImpl.kt @@ -0,0 +1,20 @@ +package com.flipperdevices.bridge.impl.manager.delegates + +import com.flipperdevices.bridge.api.manager.delegates.FlipperConnectionInformationApi +import com.flipperdevices.bridge.impl.manager.UnsafeBleManager +import kotlinx.coroutines.flow.StateFlow +import no.nordicsemi.android.ble.ktx.state.ConnectionState +import no.nordicsemi.android.ble.ktx.stateAsFlow + +class FlipperConnectionInformationApiImpl( + private val bleManager: UnsafeBleManager +) : FlipperConnectionInformationApi { + + override fun isDeviceConnected(): Boolean { + return bleManager.isConnected + } + + override fun getConnectionStateFlow(): StateFlow { + return bleManager.stateAsFlow() + } +} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/BluetoothGattServiceWrapper.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/BluetoothGattServiceWrapper.kt new file mode 100644 index 0000000000..b5b76c53d3 --- /dev/null +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/BluetoothGattServiceWrapper.kt @@ -0,0 +1,26 @@ +package com.flipperdevices.bridge.impl.manager.service + +import android.bluetooth.BluetoothGattService +import com.flipperdevices.bridge.impl.manager.UnsafeBleManager + +/** + * Delegate interface for wrap gatt services + */ +interface BluetoothGattServiceWrapper { + /** + * Call when device notify about supported service + */ + fun onServiceReceived(service: BluetoothGattService) + + /** + * Call when device is ready for reading characteristics + * Call after {@link #onServiceReceived} + */ + fun initialize(bleManager: UnsafeBleManager) + + /** + * Reset stateflows and others stateful component + * Calls after reconnect to new device or invalidate services + */ + fun reset() +} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperInformationApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperInformationApiImpl.kt new file mode 100644 index 0000000000..989886b6c7 --- /dev/null +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperInformationApiImpl.kt @@ -0,0 +1,70 @@ +package com.flipperdevices.bridge.impl.manager.service + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import com.flipperdevices.bridge.api.manager.service.FlipperInformationApi +import com.flipperdevices.bridge.api.model.FlipperGATTInformation +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.bridge.impl.manager.UnsafeBleManager +import java.util.UUID +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import timber.log.Timber + +class FlipperInformationApiImpl : BluetoothGattServiceWrapper, FlipperInformationApi { + private val informationState = MutableStateFlow(FlipperGATTInformation()) + private var infoCharacteristics = mutableMapOf() + + override fun onServiceReceived(service: BluetoothGattService) { + infoCharacteristics.putAll(service.characteristics.map { it.uuid to it }) + } + + override fun initialize(bleManager: UnsafeBleManager) { + if (infoCharacteristics == null) { + Timber.e("Info characteristics can't be null on this stage") + return + } + + bleManager.readCharacteristicUnsafe( + infoCharacteristics!![Constants.BLEInformationService.MANUFACTURER] + ).with { _, data -> + val content = data.value ?: return@with + informationState.update { + it.copy(manufacturerName = String(content)) + } + }.enqueue() + bleManager.readCharacteristicUnsafe( + infoCharacteristics!![Constants.GenericService.DEVICE_NAME] + ).with { _, data -> + val content = data.value ?: return@with + informationState.update { + it.copy(deviceName = String(content)) + } + }.enqueue() + bleManager.readCharacteristicUnsafe( + infoCharacteristics!![Constants.BLEInformationService.HARDWARE_VERSION] + ).with { _, data -> + val content = data.value ?: return@with + informationState.update { + it.copy(hardwareRevision = String(content)) + } + }.enqueue() + bleManager.readCharacteristicUnsafe( + infoCharacteristics!![Constants.BLEInformationService.SOFTWARE_VERSION] + ).with { _, data -> + val content = data.value ?: return@with + informationState.update { + it.copy(softwareVersion = String(content)) + } + }.enqueue() + } + + override fun reset() { + informationState.update { FlipperGATTInformation() } + } + + override fun getInformationFlow(): StateFlow { + return informationState + } +} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperSerialApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperSerialApiImpl.kt new file mode 100644 index 0000000000..2fb9902a50 --- /dev/null +++ b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/manager/service/FlipperSerialApiImpl.kt @@ -0,0 +1,61 @@ +package com.flipperdevices.bridge.impl.manager.service + +import android.bluetooth.BluetoothGattCharacteristic +import android.bluetooth.BluetoothGattService +import com.flipperdevices.bridge.api.manager.service.FlipperSerialApi +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.bridge.impl.manager.UnsafeBleManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch +import timber.log.Timber + +class FlipperSerialApiImpl( + private val scope: CoroutineScope +) : FlipperSerialApi, BluetoothGattServiceWrapper { + private val receiveBytesFlow = MutableSharedFlow() + + // Store bytes which pending for sending to Flipper Zero device + private val pendingBytes = mutableListOf() + + private var bleManagerInternal: UnsafeBleManager? = null + + private var serialTxCharacteristic: BluetoothGattCharacteristic? = null + private var serialRxCharacteristic: BluetoothGattCharacteristic? = null + + override fun onServiceReceived(service: BluetoothGattService) { + serialTxCharacteristic = service.getCharacteristic(Constants.BLESerialService.TX) + serialRxCharacteristic = service.getCharacteristic(Constants.BLESerialService.RX) + } + + override fun initialize(bleManager: UnsafeBleManager) { + bleManagerInternal = bleManager + bleManager.setNotificationCallbackUnsafe(serialRxCharacteristic).with { _, data -> + Timber.i("Receive serial data") + val bytes = data.value ?: return@with + scope.launch { + receiveBytesFlow.emit(bytes) + } + } + bleManager.enableNotificationsUnsafe(serialRxCharacteristic).enqueue() + bleManager.enableIndicationsUnsafe(serialRxCharacteristic).enqueue() + pendingBytes.forEach { data -> + bleManager.writeCharacteristicUnsafe(serialTxCharacteristic, data).enqueue() + } + } + + override fun reset() { + // Not exist states in this api + } + + override fun receiveBytesFlow() = receiveBytesFlow + + override fun sendBytes(data: ByteArray) { + val bleManager = bleManagerInternal + if (bleManager == null) { + pendingBytes.add(data) + return + } + bleManager.writeCharacteristicUnsafe(serialTxCharacteristic, data).enqueue() + } +} diff --git a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/pair/FlipperPairApiImpl.kt b/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/pair/FlipperPairApiImpl.kt deleted file mode 100644 index 0149bb3c2d..0000000000 --- a/components/bridge/impl/src/main/java/com/flipperdevices/bridge/impl/pair/FlipperPairApiImpl.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.flipperdevices.bridge.impl.pair - -import android.bluetooth.BluetoothAdapter -import android.bluetooth.BluetoothDevice -import android.content.Context -import com.flipperdevices.bridge.api.device.FlipperDeviceApi -import com.flipperdevices.bridge.api.pair.FlipperPairApi -import com.flipperdevices.bridge.api.scanner.FlipperScanner -import com.flipperdevices.bridge.api.utils.Constants -import com.flipperdevices.bridge.api.utils.DeviceFeatureHelper -import com.flipperdevices.bridge.api.utils.PermissionHelper -import com.flipperdevices.bridge.impl.device.FlipperDeviceApiImpl -import com.flipperdevices.bridge.impl.manager.FlipperBleManagerImpl -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.withTimeout -import no.nordicsemi.android.ble.exception.BluetoothDisabledException -import javax.inject.Inject - -class FlipperPairApiImpl @Inject constructor( - private val scanner: FlipperScanner, - private val adapter: BluetoothAdapter -) : FlipperPairApi { - override fun getFlipperApi( - context: Context, - deviceId: String - ): FlipperDeviceApi { - val manager = FlipperBleManagerImpl(context, GlobalScope) - return FlipperDeviceApiImpl(manager, deviceId) - } - - @ExperimentalCoroutinesApi - override suspend fun connect( - context: Context, - flipperDeviceApi: FlipperDeviceApi - ) { - // If we already connected to device, just ignore it - if (flipperDeviceApi.getBleManager().isDeviceConnected) { - return - } - // If Bluetooth disable, return exception - if (!PermissionHelper.isBluetoothEnabled()) { - throw BluetoothDisabledException() - } - - // If we use companion feature, we can't connect without bonded device - if (DeviceFeatureHelper.isCompanionFeatureAvailable(context)) { - connectWithBondedDevice(flipperDeviceApi) - return - } - - // If companion feature not available, we try find device in manual mode and connect with it - findAndConnectToDevice(context, flipperDeviceApi) - } - - override fun scheduleConnect(flipperDeviceApi: FlipperDeviceApi, device: BluetoothDevice) { - if (flipperDeviceApi.getBleManager().isDeviceConnected) { - return - } - flipperDeviceApi.getBleManager().connectToDevice(device) - } - - private fun connectWithBondedDevice(flipperDeviceApi: FlipperDeviceApi) { - val device = adapter.bondedDevices.find { it.address == flipperDeviceApi.address } - ?: throw IllegalArgumentException("Can't find bonded device with this id") - scheduleConnect(flipperDeviceApi, device) - } - - private suspend fun findAndConnectToDevice( - context: Context, - flipperDeviceApi: FlipperDeviceApi - ) { - if (!PermissionHelper.isPermissionGranted(context)) { - throw SecurityException( - """ - For connect to Flipper via bluetooth you need grant permission for you application. - Please, check PermissionHelper#checkPermissions - """.trimIndent() - ) - } - - val device = withTimeout(Constants.BLE.CONNECT_TIME_MS) { - scanner.findFlipperById(flipperDeviceApi.address).first() - }.device - - scheduleConnect(flipperDeviceApi, device) - } -} diff --git a/components/bridge/service/.gitignore b/components/bridge/service/api/.gitignore similarity index 100% rename from components/bridge/service/.gitignore rename to components/bridge/service/api/.gitignore diff --git a/components/bridge/service/api/build.gradle.kts b/components/bridge/service/api/build.gradle.kts new file mode 100644 index 0000000000..20c4b25faa --- /dev/null +++ b/components/bridge/service/api/build.gradle.kts @@ -0,0 +1,13 @@ +plugins { + id("com.android.library") + id("kotlin-android") +} +apply() + +dependencies { + implementation(project(":components:core")) + implementation(project(":components:bridge:api")) + + implementation(Libs.ANNOTATIONS) + implementation(Libs.APPCOMPAT) +} diff --git a/components/bridge/service/api/src/main/AndroidManifest.xml b/components/bridge/service/api/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2035c2bf56 --- /dev/null +++ b/components/bridge/service/api/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/FlipperServiceApi.kt b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/FlipperServiceApi.kt new file mode 100644 index 0000000000..d9f293247f --- /dev/null +++ b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/FlipperServiceApi.kt @@ -0,0 +1,34 @@ +package com.flipperdevices.bridge.service.api + +import android.bluetooth.BluetoothDevice +import com.flipperdevices.bridge.api.manager.FlipperRequestApi +import com.flipperdevices.bridge.api.manager.delegates.FlipperConnectionInformationApi +import com.flipperdevices.bridge.api.manager.service.FlipperInformationApi + +/** + * Provides access to the API operation of the device + * Underhood creates a service and connects to it + * + * You can get instance by FlipperServiceProvider + */ +interface FlipperServiceApi { + + /** + * Provide information about flipper name, device id + */ + val flipperInformationApi: FlipperInformationApi + + /** + * Provide information about current connection state + */ + val connectionInformationApi: FlipperConnectionInformationApi + + /** + * Returns an API for communicating with Flipper via a request-response structure. + */ + val requestApi: FlipperRequestApi + + suspend fun reconnect(deviceId: String) + + suspend fun reconnect(device: BluetoothDevice) +} diff --git a/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceConsumer.kt b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceConsumer.kt new file mode 100644 index 0000000000..c9b3c612d9 --- /dev/null +++ b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceConsumer.kt @@ -0,0 +1,15 @@ +package com.flipperdevices.bridge.service.api.provider + +import com.flipperdevices.bridge.service.api.FlipperServiceApi + +interface FlipperBleServiceConsumer { + /** + * Can be call twice or more + */ + fun onServiceApiReady(serviceApi: FlipperServiceApi) + + /** + * Called if the service throws an error + */ + fun onServiceBleError(error: FlipperBleServiceError) = Unit +} diff --git a/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceError.kt b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceError.kt new file mode 100644 index 0000000000..df19e57a06 --- /dev/null +++ b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperBleServiceError.kt @@ -0,0 +1,9 @@ +package com.flipperdevices.bridge.service.api.provider + +enum class FlipperBleServiceError { + CONNECT_BLUETOOTH_DISABLED, + CONNECT_DEVICE_NOT_STORED, + CONNECT_BLUETOOTH_PERMISSION, + CONNECT_TIMEOUT, + CONNECT_REQUIRE_REBOUND +} diff --git a/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperServiceProvider.kt b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperServiceProvider.kt new file mode 100644 index 0000000000..70a60d24c7 --- /dev/null +++ b/components/bridge/service/api/src/main/java/com/flipperdevices/bridge/service/api/provider/FlipperServiceProvider.kt @@ -0,0 +1,28 @@ +package com.flipperdevices.bridge.service.api.provider + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.flipperdevices.bridge.service.api.FlipperServiceApi + +interface FlipperServiceProvider { + /** + * ATTENTION: + * Once you no longer need the BLE, call {@link FlipperServiceApi#disconnect(FlipperBleServiceConsumer)} + * + * @param lifecycleOwner to control when we should release object + * @param onDestroyEvent on which lifecycle event we destroy connection + * @return instance for communicate with ble + */ + fun provideServiceApi( + consumer: FlipperBleServiceConsumer, + lifecycleOwner: LifecycleOwner, + onDestroyEvent: Lifecycle.Event = Lifecycle.Event.ON_DESTROY + ) + + fun provideServiceApi( + lifecycleOwner: LifecycleOwner, + onDestroyEvent: Lifecycle.Event = Lifecycle.Event.ON_DESTROY, + onError: (FlipperBleServiceError) -> Unit = {}, + onBleManager: (FlipperServiceApi) -> Unit + ) +} diff --git a/components/bridge/service/impl/.gitignore b/components/bridge/service/impl/.gitignore new file mode 100644 index 0000000000..c795b054e5 --- /dev/null +++ b/components/bridge/service/impl/.gitignore @@ -0,0 +1 @@ +build \ No newline at end of file diff --git a/components/bridge/service/build.gradle.kts b/components/bridge/service/impl/build.gradle.kts similarity index 65% rename from components/bridge/service/build.gradle.kts rename to components/bridge/service/impl/build.gradle.kts index a2f0734922..7752fcbffd 100644 --- a/components/bridge/service/build.gradle.kts +++ b/components/bridge/service/impl/build.gradle.kts @@ -1,6 +1,8 @@ plugins { id("com.android.library") id("kotlin-android") + id("com.squareup.anvil") + id("kotlin-kapt") } apply() @@ -8,14 +10,23 @@ dependencies { implementation(project(":components:core")) implementation(project(":components:bridge:provider")) implementation(project(":components:bridge:protobuf")) + implementation(project(":components:bridge:service:api")) + implementation(project(":components:bridge:impl")) implementation(Libs.ANNOTATIONS) + implementation(Libs.APPCOMPAT) + implementation(Libs.KOTLIN_COROUTINES) implementation(Libs.LIFECYCLE_RUNTIME_KTX) implementation(Libs.LIFECYCLE_VIEWMODEL_KTX) + implementation(Libs.LIFECYCLE_SERVICE) + kapt(Libs.LIFECYCLE_KAPT) implementation(Libs.NORDIC_BLE) implementation(Libs.NORDIC_BLE_KTX) implementation(Libs.NORDIC_BLE_COMMON) implementation(Libs.NORDIC_BLE_SCAN) + + implementation(Libs.DAGGER) + kapt(Libs.DAGGER_COMPILER) } diff --git a/components/bridge/service/impl/src/main/AndroidManifest.xml b/components/bridge/service/impl/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..10af88374b --- /dev/null +++ b/components/bridge/service/impl/src/main/AndroidManifest.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperService.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperService.kt new file mode 100644 index 0000000000..b0ebf63be4 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperService.kt @@ -0,0 +1,74 @@ +package com.flipperdevices.bridge.service.impl + +import android.content.Intent +import android.os.Binder +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.impl.notification.FLIPPER_NOTIFICATION_ID +import com.flipperdevices.bridge.service.impl.notification.FlipperNotificationHelper +import com.flipperdevices.bridge.service.impl.provider.error.CompositeFlipperServiceErrorListener +import com.flipperdevices.bridge.service.impl.provider.error.CompositeFlipperServiceErrorListenerImpl +import java.util.concurrent.atomic.AtomicBoolean +import kotlinx.coroutines.launch +import timber.log.Timber + +class FlipperService : LifecycleService() { + private val listener = CompositeFlipperServiceErrorListenerImpl() + private val serviceApi by lazy { FlipperServiceApiImpl(this, this, listener) } + private val binder by lazy { FlipperServiceBinder(serviceApi, listener) } + private val stopped = AtomicBoolean(false) + private lateinit var flipperNotification: FlipperNotificationHelper + + override fun onCreate() { + super.onCreate() + Timber.d("Start flipper service") + + flipperNotification = FlipperNotificationHelper(this) + startForeground(FLIPPER_NOTIFICATION_ID, flipperNotification.show()) + if (!BuildConfig.INTERNAL) { + flipperNotification.showStopButton() + } + + serviceApi.internalInit() + } + + override fun onBind(intent: Intent): Binder { + super.onBind(intent) + return binder + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + Timber.d("Service receive command with action ${intent?.action}") + + if (intent?.action == ACTION_STOP) { + stopSelfInternal() + } + + return super.onStartCommand(intent, flags, startId) + } + + override fun onDestroy() { + super.onDestroy() + Timber.d("Destroy flipper service") + } + + private fun stopSelfInternal() = lifecycleScope.launch { + if (!stopped.compareAndSet(false, true)) { + Timber.i("Service already stopped") + return@launch + } + serviceApi.close() + stopForeground(true) + stopSelf() + } + + companion object { + const val ACTION_STOP = "com.flipperdevices.bridge.service.impl.FlipperService.STOP" + } +} + +class FlipperServiceBinder( + val serviceApi: FlipperServiceApi, + compositeListener: CompositeFlipperServiceErrorListener +) : Binder(), CompositeFlipperServiceErrorListener by compositeListener 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 new file mode 100644 index 0000000000..c852427d27 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/FlipperServiceApiImpl.kt @@ -0,0 +1,84 @@ +package com.flipperdevices.bridge.service.impl + +import android.bluetooth.BluetoothDevice +import android.content.Context +import android.content.SharedPreferences +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.flipperdevices.bridge.api.manager.FlipperBleManager +import com.flipperdevices.bridge.impl.manager.FlipperBleManagerImpl +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceError +import com.flipperdevices.bridge.service.impl.delegate.FlipperServiceConnectDelegate +import com.flipperdevices.bridge.service.impl.di.FlipperServiceComponent +import com.flipperdevices.bridge.service.impl.provider.error.FlipperServiceErrorListener +import com.flipperdevices.core.di.ComponentHolder +import com.flipperdevices.core.utils.preference.FlipperSharedPreferencesKey +import javax.inject.Inject +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.launch +import no.nordicsemi.android.ble.exception.BluetoothDisabledException +import timber.log.Timber + +class FlipperServiceApiImpl( + context: Context, + lifecycleOwner: LifecycleOwner, + private val serviceErrorListener: FlipperServiceErrorListener +) : FlipperServiceApi { + @Inject + lateinit var sharedPreferences: SharedPreferences + + private val scope = lifecycleOwner.lifecycleScope + private val bleManager: FlipperBleManager = FlipperBleManagerImpl(context, scope) + private val connectDelegate = FlipperServiceConnectDelegate(bleManager, context) + + override val connectionInformationApi = bleManager.connectionInformationApi + override val requestApi = bleManager.flipperRequestApi + override val flipperInformationApi = bleManager.informationApi + + init { + ComponentHolder.component().inject(this) + } + + fun internalInit() { + connectToDeviceOnStartup() + } + + override suspend fun reconnect(deviceId: String) { + connectDelegate.reconnect(deviceId) + } + + override suspend fun reconnect(device: BluetoothDevice) { + connectDelegate.reconnect(device) + } + + suspend fun close() { + connectDelegate.disconnect() + bleManager.close() + } + + private fun connectToDeviceOnStartup() = scope.launch { + val deviceId = sharedPreferences.getString(FlipperSharedPreferencesKey.DEVICE_ID, null) + if (deviceId == null) { + Timber.e("Flipper id not found in storage") + serviceErrorListener.onError(FlipperBleServiceError.CONNECT_DEVICE_NOT_STORED) + return@launch + } + + try { + reconnect(deviceId) + } catch (securityException: SecurityException) { + serviceErrorListener.onError(FlipperBleServiceError.CONNECT_BLUETOOTH_PERMISSION) + Timber.e(securityException, "On initial connect to device") + } catch (bleDisabled: BluetoothDisabledException) { + serviceErrorListener.onError(FlipperBleServiceError.CONNECT_BLUETOOTH_DISABLED) + Timber.e(bleDisabled, "On initial connect to device") + } catch (timeout: TimeoutCancellationException) { + serviceErrorListener.onError(FlipperBleServiceError.CONNECT_TIMEOUT) + Timber.e(timeout, "On initial connect to device") + } catch (illegalArgumentException: IllegalArgumentException) { + serviceErrorListener.onError(FlipperBleServiceError.CONNECT_REQUIRE_REBOUND) + Timber.e(illegalArgumentException, "On initial connect to device") + } + } +} 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 new file mode 100644 index 0000000000..6c38ac6ffa --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/delegate/FlipperServiceConnectDelegate.kt @@ -0,0 +1,89 @@ +package com.flipperdevices.bridge.service.impl.delegate + +import android.bluetooth.BluetoothDevice +import android.content.Context +import com.flipperdevices.bridge.api.manager.FlipperBleManager +import com.flipperdevices.bridge.api.utils.Constants +import com.flipperdevices.bridge.api.utils.DeviceFeatureHelper +import com.flipperdevices.bridge.api.utils.PermissionHelper +import com.flipperdevices.bridge.provider.FlipperApi +import com.flipperdevices.bridge.service.impl.di.FlipperServiceComponent +import com.flipperdevices.core.di.ComponentHolder +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import no.nordicsemi.android.ble.exception.BluetoothDisabledException + +class FlipperServiceConnectDelegate( + private val bleManager: FlipperBleManager, + private val context: Context +) { + private val scanner = FlipperApi.flipperScanner + private val adapter = FlipperApi.bluetoothAdapter + + init { + ComponentHolder.component().inject(this) + } + + suspend fun reconnect(deviceId: String) { + // If we already connected to device, just ignore it + if (bleManager.connectionInformationApi.isDeviceConnected()) { + bleManager.disconnectDevice() + } + // If Bluetooth disable, return exception + if (!PermissionHelper.isBluetoothEnabled()) { + throw BluetoothDisabledException() + } + + // If we use companion feature, we can't connect without bonded device + if (DeviceFeatureHelper.isCompanionFeatureAvailable(context)) { + connectWithBondedDevice(deviceId) + return + } + // If companion feature not available, we try find device in manual mode and connect with it + findAndConnectToDevice(context, deviceId) + } + + suspend fun reconnect(device: BluetoothDevice) { + // If we already connected to device, just ignore it + if (bleManager.connectionInformationApi.isDeviceConnected()) { + bleManager.disconnectDevice() + } + + // If Bluetooth disable, return exception + if (!PermissionHelper.isBluetoothEnabled()) { + throw BluetoothDisabledException() + } + + bleManager.connectToDevice(device) + } + + suspend fun disconnect() { + bleManager.disconnectDevice() + } + + private suspend fun connectWithBondedDevice(deviceId: String) { + val device = adapter.bondedDevices.find { it.address == deviceId } + ?: throw IllegalArgumentException("Can't find bonded device with this id") + bleManager.connectToDevice(device) + } + + private suspend fun findAndConnectToDevice( + context: Context, + deviceId: String + ) { + if (!PermissionHelper.isPermissionGranted(context)) { + throw SecurityException( + """ + For connect to Flipper via bluetooth you need grant permission for you application. + Please, check PermissionHelper#checkPermissions + """.trimIndent() + ) + } + + val device = withTimeout(Constants.BLE.CONNECT_TIME_MS) { + scanner.findFlipperById(deviceId).first() + }.device + + bleManager.connectToDevice(device) + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperServiceComponent.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperServiceComponent.kt new file mode 100644 index 0000000000..82c2334c27 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/di/FlipperServiceComponent.kt @@ -0,0 +1,12 @@ +package com.flipperdevices.bridge.service.impl.di + +import com.flipperdevices.bridge.service.impl.FlipperServiceApiImpl +import com.flipperdevices.bridge.service.impl.delegate.FlipperServiceConnectDelegate +import com.flipperdevices.core.di.AppGraph +import com.squareup.anvil.annotations.ContributesTo + +@ContributesTo(AppGraph::class) +interface FlipperServiceComponent { + fun inject(delegate: FlipperServiceConnectDelegate) + fun inject(serviceApi: FlipperServiceApiImpl) +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperDisconnectBroadcastReceiver.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperDisconnectBroadcastReceiver.kt new file mode 100644 index 0000000000..22c33ad445 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperDisconnectBroadcastReceiver.kt @@ -0,0 +1,35 @@ +package com.flipperdevices.bridge.service.impl.notification + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import com.flipperdevices.bridge.service.impl.FlipperService + +class FlipperDisconnectBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val stopIntent = Intent(context, FlipperService::class.java).apply { + action = FlipperService.ACTION_STOP + } + context.startService(stopIntent) + } + + companion object { + private const val ACTION = + "com.flipperdevices.bridge.service.impl.notification.DisconnectBroadcastReceiver" + + @SuppressLint("UnspecifiedImmutableFlag") + fun getDisconnectIntent(context: Context): PendingIntent { + val intent = Intent(context, FlipperDisconnectBroadcastReceiver::class.java) + intent.action = ACTION + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) + } else { + PendingIntent.getBroadcast(context, 0, intent, 0) + } + } + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperNotificationHelper.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperNotificationHelper.kt new file mode 100644 index 0000000000..d01ec563ec --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/notification/FlipperNotificationHelper.kt @@ -0,0 +1,61 @@ +package com.flipperdevices.bridge.service.impl.notification + +import android.app.Notification +import android.content.Context +import androidx.core.app.NotificationChannelCompat +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.flipperdevices.bridge.service.impl.R + +private const val FLIPPER_NOTIFICATION_CHANNEL = "flipper_service" +const val FLIPPER_NOTIFICATION_ID = 1 + +class FlipperNotificationHelper(private val context: Context) { + private val notificationBuilder = + NotificationCompat.Builder(context, FLIPPER_NOTIFICATION_CHANNEL) + .setContentTitle(context.getString(R.string.bridge_service_notification_title)) + .setContentText(context.getString(R.string.bridge_service_notification_desc)) + .setSmallIcon(R.drawable.ic_notification) + .setSilent(true) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + private val notificationManager = NotificationManagerCompat.from(context) + + fun showStopButton() { + notificationBuilder.addAction( + R.drawable.ic_disconnect, + context.getString(R.string.bridge_service_notification_action_disconnect), + FlipperDisconnectBroadcastReceiver.getDisconnectIntent(context) + ) + buildAndNotify() + } + + fun showInfiniteProgressBar() { + notificationBuilder.setProgress(0, 0, true) + buildAndNotify() + } + + fun show(): Notification { + return buildAndNotify() + } + + private fun buildAndNotify(): Notification { + createChannelIfNotYet(context) + + val notification = notificationBuilder.build() + notificationManager.notify(FLIPPER_NOTIFICATION_ID, notification) + return notification + } + + private fun createChannelIfNotYet(context: Context) { + val notificationManager = NotificationManagerCompat.from(context) + + val flipperChannel = NotificationChannelCompat.Builder( + FLIPPER_NOTIFICATION_CHANNEL, + NotificationManagerCompat.IMPORTANCE_LOW + ).setName(context.getString(R.string.bridge_service_notification_channel_name)) + .setDescription(context.getString(R.string.bridge_service_notification_channel_desc)) + .build() + + notificationManager.createNotificationChannel(flipperChannel) + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/FlipperServiceProviderImpl.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/FlipperServiceProviderImpl.kt new file mode 100644 index 0000000000..638f3c610c --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/FlipperServiceProviderImpl.kt @@ -0,0 +1,140 @@ +package com.flipperdevices.bridge.service.impl.provider + +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.content.Context.BIND_IMPORTANT +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceError +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.bridge.service.impl.FlipperService +import com.flipperdevices.bridge.service.impl.FlipperServiceBinder +import com.flipperdevices.bridge.service.impl.provider.error.FlipperServiceErrorListener +import com.flipperdevices.bridge.service.impl.utils.subscribeOnFirst +import com.flipperdevices.core.di.AppGraph +import com.squareup.anvil.annotations.ContributesBinding +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +@ContributesBinding(AppGraph::class, FlipperServiceProvider::class) +@Suppress("TooManyFunctions") +class FlipperServiceProviderImpl @Inject constructor( + private val applicationContext: Context +) : FlipperServiceProvider, ServiceConnection, FlipperServiceErrorListener { + private var serviceBinder: FlipperServiceBinder? = null + + // true if we wait bind answer from android + private var isRequestedForBind: Boolean = false + private val serviceConsumers = mutableListOf() + + @Synchronized + override fun provideServiceApi( + consumer: FlipperBleServiceConsumer, + lifecycleOwner: LifecycleOwner, + onDestroyEvent: Lifecycle.Event + ) { + serviceConsumers.add(consumer) + lifecycleOwner.subscribeOnFirst(onDestroyEvent) { disconnectInternal(consumer) } + + invalidate() + serviceBinder?.let { consumer.onServiceApiReady(it.serviceApi) } + } + + @Synchronized + override fun provideServiceApi( + lifecycleOwner: LifecycleOwner, + onDestroyEvent: Lifecycle.Event, + onError: (FlipperBleServiceError) -> Unit, + onBleManager: (FlipperServiceApi) -> Unit + ) { + val consumer = LambdaFlipperBleServiceConsumer(onBleManager, onError) + provideServiceApi(consumer, lifecycleOwner, onDestroyEvent) + } + + @Synchronized + private fun invalidate() { + // If we not found any consumers, close ble connection and service + if (serviceConsumers.isEmpty()) { + stopServiceInternal() + return + } + + // If we have consumers and binder already exist, just do nothing + if (serviceBinder != null) { + return + } + + // If we already request bind, just do nothing + if (isRequestedForBind) { + return + } + + applicationContext.bindService( + Intent(applicationContext, FlipperService::class.java), this, + BIND_AUTO_CREATE or BIND_IMPORTANT + ) + isRequestedForBind = true + } + + private fun disconnectInternal(consumer: FlipperBleServiceConsumer) { + serviceConsumers.remove(consumer) + invalidate() + } + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + val flipperServiceBinder = service as FlipperServiceBinder + serviceBinder = flipperServiceBinder + flipperServiceBinder.subscribe(this) + isRequestedForBind = false + invalidate() + serviceConsumers.forEach { consumer -> + consumer.onServiceApiReady(flipperServiceBinder.serviceApi) + } + } + + override fun onServiceDisconnected(name: ComponentName?) { + resetInternal() + } + + override fun onBindingDied(name: ComponentName?) { + super.onBindingDied(name) + resetInternal() + } + + override fun onNullBinding(name: ComponentName?) { + super.onNullBinding(name) + resetInternal() + } + + @Synchronized + private fun stopServiceInternal() { + serviceBinder?.unsubscribe(this) + serviceBinder = null + applicationContext.unbindService(this) + val stopIntent = Intent(applicationContext, FlipperService::class.java).apply { + action = FlipperService.ACTION_STOP + } + applicationContext.startService(stopIntent) + } + + @Synchronized + private fun resetInternal() { + serviceBinder?.unsubscribe(this) + serviceBinder = null + isRequestedForBind = false + invalidate() + } + + override fun onError(error: FlipperBleServiceError) { + serviceConsumers.forEach { consumer -> + consumer.onServiceBleError(error) + } + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/LambdaFlipperBleServiceConsumer.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/LambdaFlipperBleServiceConsumer.kt new file mode 100644 index 0000000000..894cf09e90 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/LambdaFlipperBleServiceConsumer.kt @@ -0,0 +1,19 @@ +package com.flipperdevices.bridge.service.impl.provider + +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceError + +class LambdaFlipperBleServiceConsumer( + private val onBleManager: (FlipperServiceApi) -> Unit, + private val onError: (FlipperBleServiceError) -> Unit +) : FlipperBleServiceConsumer { + override fun onServiceApiReady(serviceApi: FlipperServiceApi) { + onBleManager(serviceApi) + } + + override fun onServiceBleError(error: FlipperBleServiceError) { + super.onServiceBleError(error) + onError(error) + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListener.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListener.kt new file mode 100644 index 0000000000..4b81e81e71 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListener.kt @@ -0,0 +1,6 @@ +package com.flipperdevices.bridge.service.impl.provider.error + +interface CompositeFlipperServiceErrorListener { + fun subscribe(errorListener: FlipperServiceErrorListener) + fun unsubscribe(errorListener: FlipperServiceErrorListener) +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListenerImpl.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListenerImpl.kt new file mode 100644 index 0000000000..12acb584ad --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/CompositeFlipperServiceErrorListenerImpl.kt @@ -0,0 +1,25 @@ +package com.flipperdevices.bridge.service.impl.provider.error + +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceError + +class CompositeFlipperServiceErrorListenerImpl : + CompositeFlipperServiceErrorListener, + FlipperServiceErrorListener { + private val listeners = mutableListOf() + + override fun subscribe(errorListener: FlipperServiceErrorListener) { + if (!listeners.contains(errorListener)) { + listeners.add(errorListener) + } + } + + override fun unsubscribe(errorListener: FlipperServiceErrorListener) { + listeners.remove(errorListener) + } + + override fun onError(error: FlipperBleServiceError) { + listeners.forEach { + it.onError(error) + } + } +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/FlipperServiceErrorListener.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/FlipperServiceErrorListener.kt new file mode 100644 index 0000000000..db9480ce5a --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/provider/error/FlipperServiceErrorListener.kt @@ -0,0 +1,8 @@ +package com.flipperdevices.bridge.service.impl.provider.error + +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceError + +@FunctionalInterface +interface FlipperServiceErrorListener { + fun onError(error: FlipperBleServiceError) +} diff --git a/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/LifecycleKtx.kt b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/LifecycleKtx.kt new file mode 100644 index 0000000000..6a96c1ad45 --- /dev/null +++ b/components/bridge/service/impl/src/main/java/com/flipperdevices/bridge/service/impl/utils/LifecycleKtx.kt @@ -0,0 +1,28 @@ +package com.flipperdevices.bridge.service.impl.utils + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.OnLifecycleEvent + +/** + * Subscribe on first emitting {@param lifecycleEvent} + */ +fun LifecycleOwner.subscribeOnFirst( + lifecycleEvent: Lifecycle.Event, + listener: () -> Unit +) { + lateinit var observer: LifecycleObserver + @Suppress("UnusedPrivateMember") + observer = object : LifecycleObserver { + @OnLifecycleEvent(Lifecycle.Event.ON_ANY) + fun onEvent(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == lifecycleEvent) { + listener() + lifecycle.removeObserver(observer) + } + } + } + + lifecycle.addObserver(observer) +} diff --git a/components/bridge/service/impl/src/main/res/drawable/ic_disconnect.xml b/components/bridge/service/impl/src/main/res/drawable/ic_disconnect.xml new file mode 100644 index 0000000000..c75fb54da8 --- /dev/null +++ b/components/bridge/service/impl/src/main/res/drawable/ic_disconnect.xml @@ -0,0 +1,10 @@ + + + diff --git a/components/bridge/service/impl/src/main/res/drawable/ic_notification.xml b/components/bridge/service/impl/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000000..efa4ce73ed --- /dev/null +++ b/components/bridge/service/impl/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,705 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/components/bridge/service/impl/src/main/res/values/strings.xml b/components/bridge/service/impl/src/main/res/values/strings.xml new file mode 100644 index 0000000000..16e800c097 --- /dev/null +++ b/components/bridge/service/impl/src/main/res/values/strings.xml @@ -0,0 +1,13 @@ + + + The necessary permissions for work are not provided + Please, enable bluetooth and try again + We could not find the device + The device is not bounded! Please correct this by going through the binding process again. + + Flipper connection + We are now connected to Flipper Zero + Disconnect + Flipper synchronization + Under this channel, you will see notifications informing you of the current synchronization status + \ No newline at end of file diff --git a/components/bridge/service/src/main/AndroidManifest.xml b/components/bridge/service/src/main/AndroidManifest.xml deleted file mode 100644 index 5cd4e65fba..0000000000 --- a/components/bridge/service/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModel.kt b/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModel.kt deleted file mode 100644 index f2afab3326..0000000000 --- a/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModel.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.flipperdevices.service - -import android.app.Application -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import com.flipperdevices.bridge.api.manager.FlipperBleManager -import com.flipperdevices.bridge.api.manager.FlipperRequestApi -import com.flipperdevices.bridge.api.model.FlipperGATTInformation -import com.flipperdevices.bridge.provider.FlipperApi -import com.flipperdevices.bridge.service.R -import com.flipperdevices.core.utils.toast -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.TimeoutCancellationException -import kotlinx.coroutines.async -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import no.nordicsemi.android.ble.exception.BluetoothDisabledException -import no.nordicsemi.android.ble.ktx.state.ConnectionState -import timber.log.Timber - -class FlipperViewModel( - application: Application, - deviceId: String -) : AndroidViewModel(application) { - private val context = application - private var currentDevice = FlipperApi.flipperPairApi.getFlipperApi(context, deviceId) - private val deviceInformation = MutableStateFlow(FlipperGATTInformation()) - private val connectionState = MutableStateFlow(null) - - fun getDeviceInformation(): StateFlow { - return deviceInformation - } - - fun getRequestApi(): FlipperRequestApi { - return currentDevice.getBleManager().flipperRequestApi - } - - fun getConnectionState(): StateFlow = connectionState - - fun connectAndStart() = viewModelScope.launch { - val bleManager = currentDevice.getBleManager() - async { subscribeToInformationState(bleManager) } - async { subscribeToConnectionState(bleManager) } - try { - FlipperApi.flipperPairApi.connect(context, currentDevice) - } catch (securityException: SecurityException) { - context.toast(R.string.info_pair_err_permission) - Timber.e(securityException) - } catch (bleDisabled: BluetoothDisabledException) { - context.toast(R.string.info_pair_err_ble_disabled) - Timber.e(bleDisabled) - } catch (timeout: TimeoutCancellationException) { - context.toast(R.string.info_pair_err_timeout) - Timber.e(timeout) - } catch (illegalArgumentException: IllegalArgumentException) { - context.toast(R.string.info_pair_err_not_bounded) - Timber.e(illegalArgumentException) - } - } - - private suspend fun subscribeToInformationState(bleManager: FlipperBleManager) = - withContext(Dispatchers.IO) { - bleManager.getInformationStateFlow().collect { - deviceInformation.emit(it) - } - } - - private suspend fun subscribeToConnectionState(bleManager: FlipperBleManager) = - withContext(Dispatchers.IO) { - bleManager.getConnectionStateFlow().collect { - connectionState.emit(it) - } - } - - override fun onCleared() { - super.onCleared() - if (currentDevice?.getBleManager()?.isDeviceConnected == true) { - currentDevice?.getBleManager()?.disconnectDevice() - } - } -} diff --git a/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModelFactory.kt b/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModelFactory.kt deleted file mode 100644 index c181820f3d..0000000000 --- a/components/bridge/service/src/main/java/com/flipperdevices/service/FlipperViewModelFactory.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.flipperdevices.service - -import android.app.Application -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -class FlipperViewModelFactory( - private val application: Application, - private val deviceId: String -) : ViewModelProvider.AndroidViewModelFactory(application) { - - @Suppress("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - return FlipperViewModel(application, deviceId) as T - } -} diff --git a/components/bridge/service/src/main/res/values/strings.xml b/components/bridge/service/src/main/res/values/strings.xml deleted file mode 100644 index a73a743e2e..0000000000 --- a/components/bridge/service/src/main/res/values/strings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - The necessary permissions for work are not provided - Please, enable bluetooth and try again - We could not find the device - The device is not bounded! Please correct this by going through the binding process again. - \ No newline at end of file diff --git a/components/core/src/main/java/com/flipperdevices/core/navigation/screen/InfoScreenProvider.kt b/components/core/src/main/java/com/flipperdevices/core/navigation/screen/InfoScreenProvider.kt index e0134ae084..e7f5142e1c 100644 --- a/components/core/src/main/java/com/flipperdevices/core/navigation/screen/InfoScreenProvider.kt +++ b/components/core/src/main/java/com/flipperdevices/core/navigation/screen/InfoScreenProvider.kt @@ -6,5 +6,5 @@ import com.github.terrakok.cicerone.Screen * Provide screens for info components */ interface InfoScreenProvider { - fun deviceInformationScreen(deviceId: String): Screen + fun deviceInformationScreen(): Screen } diff --git a/components/core/src/main/java/com/flipperdevices/core/utils/ExecutorExt.kt b/components/core/src/main/java/com/flipperdevices/core/utils/ExecutorExt.kt new file mode 100644 index 0000000000..2bce18eefb --- /dev/null +++ b/components/core/src/main/java/com/flipperdevices/core/utils/ExecutorExt.kt @@ -0,0 +1,10 @@ +package com.flipperdevices.core.utils + +import java.util.concurrent.Executor +import java.util.concurrent.Executors + +fun newSingleThreadExecutor(name: String): Executor { + return Executors.newSingleThreadExecutor { + Thread(it, name) + } +} diff --git a/components/core/src/main/java/com/flipperdevices/core/utils/preference/FlipperSharedPreferencesKey.kt b/components/core/src/main/java/com/flipperdevices/core/utils/preference/FlipperSharedPreferencesKey.kt index a30803e2dd..44023c6bb7 100644 --- a/components/core/src/main/java/com/flipperdevices/core/utils/preference/FlipperSharedPreferencesKey.kt +++ b/components/core/src/main/java/com/flipperdevices/core/utils/preference/FlipperSharedPreferencesKey.kt @@ -1,5 +1,5 @@ package com.flipperdevices.core.utils.preference object FlipperSharedPreferencesKey { - const val DEVICE_ID = "selected_device_id" + const val DEVICE_ID = "selected_device_id_v2" } diff --git a/components/core/src/main/java/com/flipperdevices/core/view/LifecycleViewModel.kt b/components/core/src/main/java/com/flipperdevices/core/view/LifecycleViewModel.kt new file mode 100644 index 0000000000..4341ad8034 --- /dev/null +++ b/components/core/src/main/java/com/flipperdevices/core/view/LifecycleViewModel.kt @@ -0,0 +1,29 @@ +package com.flipperdevices.core.view + +import androidx.annotation.CallSuper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleRegistry +import androidx.lifecycle.ViewModel + +abstract class LifecycleViewModel : ViewModel(), LifecycleOwner { + private val registry by lazy { LifecycleRegistry(this) } + + init { + registry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) + registry.handleLifecycleEvent(Lifecycle.Event.ON_START) + registry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME) + } + + override fun getLifecycle(): Lifecycle { + return registry + } + + @CallSuper + override fun onCleared() { + super.onCleared() + registry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE) + registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP) + registry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) + } +} diff --git a/components/filemanager/api/src/main/java/com/flipperdevices/filemanager/api/navigation/FileManagerScreenProvider.kt b/components/filemanager/api/src/main/java/com/flipperdevices/filemanager/api/navigation/FileManagerScreenProvider.kt index fba20099a6..7926943254 100644 --- a/components/filemanager/api/src/main/java/com/flipperdevices/filemanager/api/navigation/FileManagerScreenProvider.kt +++ b/components/filemanager/api/src/main/java/com/flipperdevices/filemanager/api/navigation/FileManagerScreenProvider.kt @@ -3,5 +3,5 @@ package com.flipperdevices.filemanager.api.navigation import com.github.terrakok.cicerone.Screen interface FileManagerScreenProvider { - fun fileManager(deviceId: String, path: String = "/ext/"): Screen + fun fileManager(path: String): Screen } diff --git a/components/filemanager/impl/build.gradle.kts b/components/filemanager/impl/build.gradle.kts index fb587a3351..ae1b619ed6 100644 --- a/components/filemanager/impl/build.gradle.kts +++ b/components/filemanager/impl/build.gradle.kts @@ -9,7 +9,7 @@ apply() dependencies { implementation(project(":components:core")) - implementation(project(":components:bridge:service")) + implementation(project(":components:bridge:service:api")) implementation(project(":components:bridge:api")) implementation(project(":components:bridge:protobuf")) implementation(project(":components:filemanager:api")) diff --git a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/di/FileManagerComponent.kt b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/di/FileManagerComponent.kt index 304e590705..3506cffa64 100644 --- a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/di/FileManagerComponent.kt +++ b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/di/FileManagerComponent.kt @@ -2,9 +2,11 @@ package com.flipperdevices.filemanager.impl.di import com.flipperdevices.core.di.AppGraph import com.flipperdevices.filemanager.impl.fragment.FileManagerFragment +import com.flipperdevices.filemanager.impl.viewmodels.FileManagerViewModel import com.squareup.anvil.annotations.ContributesTo @ContributesTo(AppGraph::class) interface FileManagerComponent { fun inject(fileManagerFragment: FileManagerFragment) + fun inject(viewModel: FileManagerViewModel) } diff --git a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/fragment/FileManagerFragment.kt b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/fragment/FileManagerFragment.kt index babc3f6028..87215d20d8 100644 --- a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/fragment/FileManagerFragment.kt +++ b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/fragment/FileManagerFragment.kt @@ -1,45 +1,30 @@ package com.flipperdevices.filemanager.impl.fragment import android.os.Bundle -import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope +import androidx.fragment.app.viewModels import com.flipperdevices.core.di.ComponentHolder import com.flipperdevices.core.view.ComposeFragment import com.flipperdevices.filemanager.api.navigation.FileManagerScreenProvider import com.flipperdevices.filemanager.impl.composable.ComposableFileManager import com.flipperdevices.filemanager.impl.di.FileManagerComponent -import com.flipperdevices.filemanager.impl.model.FileItem -import com.flipperdevices.protobuf.main -import com.flipperdevices.protobuf.storage.Storage -import com.flipperdevices.protobuf.storage.listRequest -import com.flipperdevices.service.FlipperViewModel -import com.flipperdevices.service.FlipperViewModelFactory +import com.flipperdevices.filemanager.impl.viewmodels.FileManagerViewModel +import com.flipperdevices.filemanager.impl.viewmodels.FileManagerViewModelFactory import com.github.terrakok.cicerone.Router import java.io.File import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.runningReduce -import kotlinx.coroutines.launch -import timber.log.Timber class FileManagerFragment : ComposeFragment() { - private val stateFlow = MutableStateFlow>(emptyList()) - @Inject lateinit var router: Router @Inject lateinit var screenProvider: FileManagerScreenProvider - private val bleViewModel by activityViewModels { - FlipperViewModelFactory(requireActivity().application, getDeviceId()) + private val viewModel by viewModels() { + FileManagerViewModelFactory(getDirectory()) } override fun onCreate(savedInstanceState: Bundle?) { @@ -49,52 +34,18 @@ class FileManagerFragment : ComposeFragment() { @Composable override fun renderView() { - val fileList by stateFlow.collectAsState(emptyList()) + val fileList by viewModel.getFileList().collectAsState() ComposableFileManager(fileList) { val newPath = File(getDirectory(), it.fileName).absolutePath - router.navigateTo(screenProvider.fileManager(getDeviceId(), newPath)) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - lifecycleScope.launch { - getFilesForDir(getDirectory()).collect { - stateFlow.emit(it) - } + router.navigateTo(screenProvider.fileManager(newPath)) } } - private fun getFilesForDir(directory: String): Flow> { - return bleViewModel.getRequestApi().request( - main { - storageListRequest = listRequest { - path = directory - } - } - ).map { - Timber.i("FileManagerFragment#$directory") - it.storageListResponse.fileList.map { file -> - FileItem( - fileName = file.name, - isDirectory = file.type == Storage.File.FileType.DIR, - size = file.size.toLong() - ) - } - }.runningReduce { accumulator, value -> accumulator.plus(value) } - } - companion object { - const val EXTRA_DEVICE_KEY = "device_id" const val EXTRA_DIRECTORY_KEY = "directory" } - private fun getDeviceId(): String { - return arguments?.get(EXTRA_DEVICE_KEY) as String - } - private fun getDirectory(): String { return arguments?.get(EXTRA_DIRECTORY_KEY) as String } diff --git a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/navigation/FileManagerScreenProviderImpl.kt b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/navigation/FileManagerScreenProviderImpl.kt index f1df0fffec..83b4351cac 100644 --- a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/navigation/FileManagerScreenProviderImpl.kt +++ b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/navigation/FileManagerScreenProviderImpl.kt @@ -11,10 +11,9 @@ import javax.inject.Inject @ContributesBinding(AppGraph::class) class FileManagerScreenProviderImpl @Inject constructor() : FileManagerScreenProvider { - override fun fileManager(deviceId: String, path: String): Screen { - return FragmentScreen("FileManager_${deviceId}_$path") { + override fun fileManager(path: String): Screen { + return FragmentScreen("FileManager_$path") { FileManagerFragment().withArgs { - putString(FileManagerFragment.EXTRA_DEVICE_KEY, deviceId) putString(FileManagerFragment.EXTRA_DIRECTORY_KEY, path) } } diff --git a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModel.kt b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModel.kt new file mode 100644 index 0000000000..bd6348f3b1 --- /dev/null +++ b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModel.kt @@ -0,0 +1,61 @@ +package com.flipperdevices.filemanager.impl.viewmodels + +import androidx.lifecycle.viewModelScope +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.di.ComponentHolder +import com.flipperdevices.core.view.LifecycleViewModel +import com.flipperdevices.filemanager.impl.di.FileManagerComponent +import com.flipperdevices.filemanager.impl.model.FileItem +import com.flipperdevices.protobuf.main +import com.flipperdevices.protobuf.storage.Storage +import com.flipperdevices.protobuf.storage.listRequest +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.runningReduce +import kotlinx.coroutines.launch +import timber.log.Timber + +class FileManagerViewModel( + private val directory: String +) : LifecycleViewModel(), FlipperBleServiceConsumer { + @Inject + lateinit var serviceProvider: FlipperServiceProvider + + private var fileListStateFlow = MutableStateFlow>(emptyList()) + + init { + ComponentHolder.component().inject(this) + serviceProvider.provideServiceApi(consumer = this, lifecycleOwner = this) + } + + fun getFileList(): StateFlow> = fileListStateFlow + + override fun onServiceApiReady(serviceApi: FlipperServiceApi) { + viewModelScope.launch { + serviceApi.requestApi.request( + main { + storageListRequest = listRequest { + path = directory + } + } + ).map { + Timber.i("FileManagerFragment#$directory") + it.storageListResponse.fileList.map { file -> + FileItem( + fileName = file.name, + isDirectory = file.type == Storage.File.FileType.DIR, + size = file.size.toLong() + ) + } + }.runningReduce { accumulator, value -> accumulator.plus(value) } + .collect { + fileListStateFlow.emit(it) + } + } + } +} diff --git a/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModelFactory.kt b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModelFactory.kt new file mode 100644 index 0000000000..30a3ee1c26 --- /dev/null +++ b/components/filemanager/impl/src/main/java/com/flipperdevices/filemanager/impl/viewmodels/FileManagerViewModelFactory.kt @@ -0,0 +1,13 @@ +package com.flipperdevices.filemanager.impl.viewmodels + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class FileManagerViewModelFactory( + private val path: String +) : ViewModelProvider.NewInstanceFactory() { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return FileManagerViewModel(path) as T + } +} diff --git a/components/info/build.gradle.kts b/components/info/build.gradle.kts index d4e5629cb6..ae60039753 100644 --- a/components/info/build.gradle.kts +++ b/components/info/build.gradle.kts @@ -20,7 +20,7 @@ dependencies { implementation(project(":components:core")) implementation(project(":components:bridge:provider")) implementation(project(":components:pair:api")) - implementation(project(":components:bridge:service")) + implementation(project(":components:bridge:service:api")) implementation(Libs.COMPOSE_UI) implementation(Libs.COMPOSE_MATERIAL) diff --git a/components/info/src/main/java/com/flipperdevices/info/di/InfoComponent.kt b/components/info/src/main/java/com/flipperdevices/info/di/InfoComponent.kt index 81fc61d08e..013faf8927 100644 --- a/components/info/src/main/java/com/flipperdevices/info/di/InfoComponent.kt +++ b/components/info/src/main/java/com/flipperdevices/info/di/InfoComponent.kt @@ -2,9 +2,11 @@ package com.flipperdevices.info.di import com.flipperdevices.core.di.AppGraph import com.flipperdevices.info.main.InfoFragment +import com.flipperdevices.info.main.viewmodel.InfoViewModel import com.squareup.anvil.annotations.ContributesTo @ContributesTo(AppGraph::class) interface InfoComponent { fun inject(fragment: InfoFragment) + fun inject(viewModel: InfoViewModel) } diff --git a/components/info/src/main/java/com/flipperdevices/info/main/InfoFragment.kt b/components/info/src/main/java/com/flipperdevices/info/main/InfoFragment.kt index 0eb9a7fa08..6b0c76bf80 100644 --- a/components/info/src/main/java/com/flipperdevices/info/main/InfoFragment.kt +++ b/components/info/src/main/java/com/flipperdevices/info/main/InfoFragment.kt @@ -1,27 +1,23 @@ package com.flipperdevices.info.main import android.os.Bundle -import android.view.View import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.fragment.app.activityViewModels +import androidx.fragment.app.viewModels import com.flipperdevices.core.di.ComponentHolder import com.flipperdevices.core.view.ComposeFragment import com.flipperdevices.info.di.InfoComponent import com.flipperdevices.info.main.compose.ComposeInfoScreen +import com.flipperdevices.info.main.viewmodel.InfoViewModel import com.flipperdevices.pair.api.PairComponentApi -import com.flipperdevices.service.FlipperViewModel -import com.flipperdevices.service.FlipperViewModelFactory import javax.inject.Inject class InfoFragment : ComposeFragment() { @Inject lateinit var pairComponentApi: PairComponentApi - private val bleViewModel by activityViewModels { - FlipperViewModelFactory(requireActivity().application, getDeviceId()) - } + private val viewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -30,21 +26,8 @@ class InfoFragment : ComposeFragment() { @Composable override fun renderView() { - val information by bleViewModel.getDeviceInformation().collectAsState() - val connectionState by bleViewModel.getConnectionState().collectAsState() + val information by viewModel.getDeviceInformation().collectAsState() + val connectionState by viewModel.getConnectionState().collectAsState() ComposeInfoScreen(information, connectionState) } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - bleViewModel.connectAndStart() - } - - companion object { - const val EXTRA_DEVICE_KEY = "device_id" - } - - private fun getDeviceId(): String { - return arguments?.get(EXTRA_DEVICE_KEY) as String - } } diff --git a/components/info/src/main/java/com/flipperdevices/info/main/viewmodel/InfoViewModel.kt b/components/info/src/main/java/com/flipperdevices/info/main/viewmodel/InfoViewModel.kt new file mode 100644 index 0000000000..330d80ecb9 --- /dev/null +++ b/components/info/src/main/java/com/flipperdevices/info/main/viewmodel/InfoViewModel.kt @@ -0,0 +1,46 @@ +package com.flipperdevices.info.main.viewmodel + +import androidx.lifecycle.viewModelScope +import com.flipperdevices.bridge.api.model.FlipperGATTInformation +import com.flipperdevices.bridge.service.api.FlipperServiceApi +import com.flipperdevices.bridge.service.api.provider.FlipperBleServiceConsumer +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.di.ComponentHolder +import com.flipperdevices.core.view.LifecycleViewModel +import com.flipperdevices.info.di.InfoComponent +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import no.nordicsemi.android.ble.ktx.state.ConnectionState + +class InfoViewModel : LifecycleViewModel(), FlipperBleServiceConsumer { + @Inject + lateinit var bleService: FlipperServiceProvider + + private val informationState = MutableStateFlow(FlipperGATTInformation()) + private val connectionState = MutableStateFlow(ConnectionState.Disconnecting) + + init { + ComponentHolder.component().inject(this) + bleService.provideServiceApi(consumer = this, lifecycleOwner = this) + } + + fun getDeviceInformation(): StateFlow { + return informationState + } + + fun getConnectionState(): StateFlow { + return connectionState + } + + override fun onServiceApiReady(serviceApi: FlipperServiceApi) { + serviceApi.flipperInformationApi.getInformationFlow().onEach { + informationState.emit(it) + }.launchIn(viewModelScope) + serviceApi.connectionInformationApi.getConnectionStateFlow().onEach { + connectionState.emit(it) + }.launchIn(viewModelScope) + } +} diff --git a/components/info/src/main/java/com/flipperdevices/info/navigation/InfoScreenProviderImpl.kt b/components/info/src/main/java/com/flipperdevices/info/navigation/InfoScreenProviderImpl.kt index 9754508978..f6dbd80036 100644 --- a/components/info/src/main/java/com/flipperdevices/info/navigation/InfoScreenProviderImpl.kt +++ b/components/info/src/main/java/com/flipperdevices/info/navigation/InfoScreenProviderImpl.kt @@ -2,7 +2,6 @@ package com.flipperdevices.info.navigation import com.flipperdevices.core.di.AppGraph import com.flipperdevices.core.navigation.screen.InfoScreenProvider -import com.flipperdevices.core.utils.withArgs import com.flipperdevices.info.main.InfoFragment import com.github.terrakok.cicerone.androidx.FragmentScreen import com.squareup.anvil.annotations.ContributesBinding @@ -10,10 +9,5 @@ import javax.inject.Inject @ContributesBinding(AppGraph::class) class InfoScreenProviderImpl @Inject constructor() : InfoScreenProvider { - override fun deviceInformationScreen(deviceId: String) = - FragmentScreen("Info_$deviceId") { - InfoFragment().withArgs { - putString(InfoFragment.EXTRA_DEVICE_KEY, deviceId) - } - } + override fun deviceInformationScreen() = FragmentScreen() { InfoFragment() } } diff --git a/components/pair/impl/build.gradle.kts b/components/pair/impl/build.gradle.kts index 0c800aacb2..9e0fe8667b 100644 --- a/components/pair/impl/build.gradle.kts +++ b/components/pair/impl/build.gradle.kts @@ -18,6 +18,7 @@ dependencies { implementation(project(":components:core")) implementation(project(":components:bridge:api")) implementation(project(":components:bridge:provider")) + implementation(project(":components:bridge:service:api")) implementation(project(":components:pair:api")) implementation(Libs.KOTLIN_COROUTINES) diff --git a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/di/PairComponent.kt b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/di/PairComponent.kt index 4ccf76bcb1..153766504d 100644 --- a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/di/PairComponent.kt +++ b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/di/PairComponent.kt @@ -3,6 +3,7 @@ package com.flipperdevices.pair.impl.di import com.flipperdevices.core.di.AppGraph import com.flipperdevices.pair.impl.PairScreenActivity import com.flipperdevices.pair.impl.findstandart.StandartFindFragment +import com.flipperdevices.pair.impl.findstandart.service.PairDeviceViewModel import com.flipperdevices.pair.impl.fragments.findcompanion.CompanionFindFragment import com.flipperdevices.pair.impl.fragments.guide.FragmentGuide import com.flipperdevices.pair.impl.fragments.permission.PermissionFragment @@ -19,4 +20,5 @@ interface PairComponent { fun inject(fragment: FragmentGuide) fun inject(fragment: CompanionFindFragment) fun inject(activity: PairScreenActivity) + fun inject(viewModel: PairDeviceViewModel) } diff --git a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/findstandart/service/PairDeviceViewModel.kt b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/findstandart/service/PairDeviceViewModel.kt index 9edbaaac8a..2c3b836e12 100644 --- a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/findstandart/service/PairDeviceViewModel.kt +++ b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/findstandart/service/PairDeviceViewModel.kt @@ -1,36 +1,43 @@ package com.flipperdevices.pair.impl.findstandart.service -import android.app.Application import android.bluetooth.BluetoothDevice -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope -import com.flipperdevices.bridge.provider.FlipperApi +import com.flipperdevices.bridge.api.manager.delegates.FlipperConnectionInformationApi +import com.flipperdevices.bridge.service.api.provider.FlipperServiceProvider +import com.flipperdevices.core.di.ComponentHolder +import com.flipperdevices.core.view.LifecycleViewModel +import com.flipperdevices.pair.impl.di.PairComponent import com.flipperdevices.pair.impl.model.findcompanion.PairingState +import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import no.nordicsemi.android.ble.ktx.state.ConnectionState -class PairDeviceViewModel(application: Application) : AndroidViewModel(application) { - private val context = application +class PairDeviceViewModel : LifecycleViewModel() { + @Inject + lateinit var bleService: FlipperServiceProvider private var deviceInternal: BluetoothDevice? = null + private val _state = MutableStateFlow(PairingState.NotInitialized) + init { + ComponentHolder.component().inject(this) + } + fun getConnectionState(): StateFlow = _state fun startConnectToDevice(onReady: (BluetoothDevice) -> Unit) { val device = deviceInternal ?: error("You need call #onDeviceFounded before") - val flipperDeviceApi = FlipperApi.flipperPairApi.getFlipperApi(context, device.address) - viewModelScope.launch { - flipperDeviceApi.getBleManager().getConnectionStateFlow().collect { - _state.emit(PairingState.WithDevice(it)) - if (it == ConnectionState.Ready) { - onReady(device) - } + bleService.provideServiceApi(this) { serviceApi -> + subscribeToConnectionState(serviceApi.connectionInformationApi) { + onReady(device) + } + viewModelScope.launch { + serviceApi.reconnect(device) } } - FlipperApi.flipperPairApi.scheduleConnect(flipperDeviceApi, device) } fun onDeviceFounded(device: BluetoothDevice) { @@ -48,4 +55,16 @@ class PairDeviceViewModel(application: Application) : AndroidViewModel(applicati _state.emit(PairingState.Failed(reason)) } } + + private fun subscribeToConnectionState( + informationApi: FlipperConnectionInformationApi, + onReady: () -> Unit + ) = viewModelScope.launch { + informationApi.getConnectionStateFlow().collect { + _state.emit(PairingState.WithDevice(it)) + if (it == ConnectionState.Ready) { + onReady() + } + } + } } diff --git a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/fragments/findcompanion/CompanionFindFragment.kt b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/fragments/findcompanion/CompanionFindFragment.kt index 954cc134a7..f3a0efa78b 100644 --- a/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/fragments/findcompanion/CompanionFindFragment.kt +++ b/components/pair/impl/src/main/java/com/flipperdevices/pair/impl/fragments/findcompanion/CompanionFindFragment.kt @@ -128,7 +128,10 @@ class CompanionFindFragment : ComposeFragment() { deviceManager.associate( pairingRequest, object : CompanionDeviceManager.Callback() { - override fun onDeviceFound(chooserLauncher: IntentSender) { + override fun onDeviceFound(chooserLauncher: IntentSender?) { + if (chooserLauncher == null) { + return + } val intentSenderRequest = IntentSenderRequest.Builder(chooserLauncher).build() deviceConnectWithResult.launch(intentSenderRequest) } diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index 6e7e755fb3..c06d327987 100755 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -656,7 +656,7 @@ style: RedundantVisibilityModifierRule: active: false ReturnCount: - active: true + active: false max: 2 excludedFunctions: 'equals' excludeLabeled: false diff --git a/instances/app/build.gradle.kts b/instances/app/build.gradle.kts index f3c56c6ef0..79e92e2d8f 100644 --- a/instances/app/build.gradle.kts +++ b/instances/app/build.gradle.kts @@ -15,6 +15,8 @@ dependencies { implementation(project(":components:bottombar")) implementation(project(":components:filemanager:api")) implementation(project(":components:filemanager:impl")) + implementation(project(":components:bridge:service:api")) + implementation(project(":components:bridge:service:impl")) implementation(Libs.ANNOTATIONS) implementation(Libs.CORE_KTX) diff --git a/settings.gradle.kts b/settings.gradle.kts index 4f60956b2b..5dee6df539 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,8 @@ rootProject.name = "Flipper App" include(":components:bridge:api") include(":components:bridge:impl") include(":components:bridge:provider") -include(":components:bridge:service") +include(":components:bridge:service:impl") +include(":components:bridge:service:api") include(":components:bridge:protobuf") include(":components:filemanager:api") include(":components:filemanager:impl")