Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add usb connection for sample #1021

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

# 1.8.2 - In Progress

- [Feature] Add USB connection option to sample
- [Feature] Migrate app onto new BLE api
- [Fix] Fix CI for desktop sample app


# 1.8.1

- [Feature] Add count subfolders for new file manager
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ commonDependencies {

androidDependencies {
implementation(projects.components.core.di)
implementation(projects.components.core.activityholder)
}

dependencies {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.flipperdevices.bridge.connection
import android.app.Application
import com.flipperdevices.bridge.connection.di.AppComponent
import com.flipperdevices.bridge.connection.di.DaggerMergedAndroidAppComponent
import com.flipperdevices.core.activityholder.CurrentActivityHolder
import com.flipperdevices.core.di.ApplicationParams
import com.flipperdevices.core.di.ComponentHolder
import timber.log.Timber
Expand All @@ -27,5 +28,7 @@ class ConnectionTestApplication : Application() {
ComponentHolder.components += appComponent

Timber.plant(Timber.DebugTree())

CurrentActivityHolder.register(this)
}
}
6 changes: 6 additions & 0 deletions components/bridge/connection/sample/shared/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ commonDependencies {
api(projects.components.bridge.connection.feature.screenstreaming.impl)
api(projects.components.bridge.connection.feature.update.api)
api(projects.components.bridge.connection.feature.update.impl)
api(projects.components.bridge.connection.feature.emulate.api)
api(projects.components.bridge.connection.feature.emulate.impl)

api(projects.components.filemngr.main.api)
api(projects.components.filemngr.main.impl)
Expand Down Expand Up @@ -117,6 +119,9 @@ androidDependencies {
api(projects.components.bridge.connection.transport.ble.api)
api(projects.components.bridge.connection.transport.ble.impl)

api(projects.components.bridge.connection.transport.usb.api)
api(projects.components.bridge.connection.transport.usb.impl)

api(projects.components.firstpair.connection.api)

api(projects.components.keyparser.api)
Expand All @@ -133,4 +138,5 @@ androidDependencies {
api(libs.ble.kotlin.client)

api(libs.compose.activity)
implementation(libs.usb.android)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,14 @@ import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage
import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroBleModel
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.preference.pb.FlipperZeroBle
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
Expand All @@ -22,14 +26,13 @@ import no.nordicsemi.android.kotlin.ble.core.scanner.BleScanMode
import no.nordicsemi.android.kotlin.ble.core.scanner.BleScannerSettings
import no.nordicsemi.android.kotlin.ble.scanner.BleScanner
import no.nordicsemi.android.kotlin.ble.scanner.aggregator.BleScanResultAggregator
import javax.inject.Inject

@SuppressLint("MissingPermission")
@ContributesBinding(AppGraph::class, ConnectionSearchViewModel::class)
class BLESearchViewModel @Inject constructor(
class BLESearchConnectionDelegate @AssistedInject constructor(
@Assisted viewModelScope: CoroutineScope,
context: Context,
persistedStorage: FDevicePersistedStorage
) : ConnectionSearchViewModel(persistedStorage) {
) : ConnectionSearchDelegate {
private val aggregator = BleScanResultAggregator()
private val devicesFlow = MutableStateFlow<PersistentList<ConnectionSearchItem>>(
persistentListOf()
Expand Down Expand Up @@ -62,6 +65,12 @@ class BLESearchViewModel @Inject constructor(
}

override fun getDevicesFlow() = devicesFlow.asStateFlow()

@AssistedFactory
@ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class)
fun interface Factory : ConnectionSearchDelegate.Factory {
override fun invoke(scope: CoroutineScope): BLESearchConnectionDelegate
}
}

private fun ServerDevice.toFDeviceFlipperZeroBleModel() = FDeviceFlipperZeroBleModel(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package com.flipperdevices.bridge.connection.screens.search

import android.content.Context
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage
import com.flipperdevices.bridge.connection.config.api.model.FDeviceFlipperZeroUsbModel
import com.flipperdevices.core.di.AppGraph
import com.flipperdevices.core.log.LogTagProvider
import com.flipperdevices.core.log.info
import com.hoho.android.usbserial.driver.UsbSerialProber
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

private val FLIPPER_NAME_REGEXP = "Flipper ([A-Za-z]+)".toRegex()

class USBSearchDelegate @AssistedInject constructor(
@Assisted viewModelScope: CoroutineScope,
private val context: Context,
private val persistedStorage: FDevicePersistedStorage
) : ConnectionSearchDelegate, LogTagProvider {
override val TAG = "USBSearchViewModel"

private val usbManager = context.getSystemService(Context.USB_SERVICE) as UsbManager

private val searchItems =
MutableStateFlow<ImmutableList<ConnectionSearchItem>>(persistentListOf())

init {
viewModelScope.launch {
combine(
flow {
while (true) {
emit(Unit)
kotlinx.coroutines.delay(1.seconds)
}
},
persistedStorage.getAllDevices()
) { _, savedDevices ->
UsbSerialProber
.getDefaultProber()
.findAllDrivers(usbManager)
.map { it.device } to savedDevices
}.collect { (searchDevices, savedDevices) ->
val existedDescriptors = savedDevices
.filterIsInstance<FDeviceFlipperZeroUsbModel>()
.associateBy { it.portPath }

info { searchDevices.joinToString(",") { "$it" } }

searchItems.emit(
searchDevices.map { it.toFDeviceFlipperZeroUSBModel() }.map { usbDevice ->
ConnectionSearchItem(
address = usbDevice.portPath,
deviceModel = existedDescriptors[usbDevice.portPath]
?: usbDevice,
isAdded = existedDescriptors.containsKey(usbDevice.portPath)
)
}.distinctBy { it.address }
.toImmutableList()

)
}
}
}

override fun getDevicesFlow() = searchItems.asStateFlow()

@AssistedFactory
@ContributesMultibinding(AppGraph::class, ConnectionSearchDelegate.Factory::class)
fun interface Factory : ConnectionSearchDelegate.Factory {
override fun invoke(scope: CoroutineScope): USBSearchDelegate
}
}

private fun UsbDevice.toFDeviceFlipperZeroUSBModel(): FDeviceFlipperZeroUsbModel {
return FDeviceFlipperZeroUsbModel(
name = productName?.extractFlipperName() ?: deviceName,
portPath = deviceId.toString(),
humanReadableName = productName ?: deviceName,
)
}

private fun String.extractFlipperName(): String {
val regexFind = FLIPPER_NAME_REGEXP.find(this)
val groups = regexFind?.groupValues
return groups?.getOrNull(index = 1)
?: this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M440,880v-304L256,760l-56,-56 224,-224 -224,-224 56,-56 184,184v-304h40l228,228 -172,172 172,172L480,880h-40ZM520,384 L596,308 520,234v150ZM520,726 L596,652 520,576v150Z"
android:fillColor="#5f6368"/>
</vector>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M480,880q-33,0 -56.5,-23.5T400,800q0,-21 11,-39t29,-29v-92L320,640q-33,0 -56.5,-23.5T240,560v-92q-18,-9 -29,-27t-11,-41q0,-33 23.5,-56.5T280,320q33,0 56.5,23.5T360,400q0,23 -11,40t-29,28v92h120v-320h-80l120,-160 120,160h-80v320h120v-80h-40v-160h160v160h-40v80q0,33 -23.5,56.5T640,640L520,640v92q19,10 29.5,28t10.5,40q0,33 -23.5,56.5T480,880Z"
android:fillColor="#5f6368"/>
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import flipperapp.components.bridge.connection.sample.shared.generated.resources.Res
import flipperapp.components.bridge.connection.sample.shared.generated.resources.connection_search_title
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_add_box
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_delete
import flipperapp.components.core.ui.res.generated.resources.material_ic_close
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
Expand Down Expand Up @@ -68,31 +66,10 @@ class ConnectionSearchDecomposeComponent @AssistedInject constructor(
devices,
key = { device -> device.address }
) { searchItem ->
Row {
Text(
modifier = Modifier
.weight(1f)
.padding(16.dp),
text = searchItem.deviceModel.humanReadableName,
color = LocalPallet.current.text100
)

Icon(
modifier = Modifier
.clickableRipple { searchViewModel.onDeviceClick(searchItem) }
.padding(16.dp)
.size(24.dp),
painter = painterResource(
if (searchItem.isAdded) {
Res.drawable.material_ic_delete
} else {
Res.drawable.material_ic_add_box
}
),
contentDescription = null,
tint = LocalPallet.current.text100
)
}
ConnectionSearchItemComposable(
searchItem,
onDeviceClick = { searchViewModel.onDeviceClick(searchItem) }
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.flipperdevices.bridge.connection.screens.search

import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow

interface ConnectionSearchDelegate {
fun getDevicesFlow(): StateFlow<ImmutableList<ConnectionSearchItem>>
fun interface Factory {
operator fun invoke(
scope: CoroutineScope,
): ConnectionSearchDelegate
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.flipperdevices.bridge.connection.screens.search

import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.flipperdevices.bridge.connection.config.api.FDeviceType
import com.flipperdevices.core.ui.ktx.clickableRipple
import com.flipperdevices.core.ui.theme.LocalPallet
import flipperapp.components.bridge.connection.sample.shared.generated.resources.Res
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_add_box
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_bluetooth
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_delete
import flipperapp.components.bridge.connection.sample.shared.generated.resources.material_ic_usb
import org.jetbrains.compose.resources.painterResource

@Composable
fun ConnectionSearchItemComposable(
searchItem: ConnectionSearchItem,
onDeviceClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier) {
Icon(
modifier = Modifier
.padding(16.dp)
.size(24.dp),
painter = painterResource(
when (searchItem.deviceModel.type) {
FDeviceType.FLIPPER_ZERO_BLE -> Res.drawable.material_ic_bluetooth
FDeviceType.FLIPPER_ZERO_USB -> Res.drawable.material_ic_usb
}
),
contentDescription = null,
tint = LocalPallet.current.text100
)

Text(
modifier = Modifier
.weight(1f)
.padding(16.dp),
text = searchItem.deviceModel.humanReadableName,
color = LocalPallet.current.text100
)

Icon(
modifier = Modifier
.clickableRipple(onClick = onDeviceClick)
.padding(16.dp)
.size(24.dp),
painter = painterResource(
if (searchItem.isAdded) {
Res.drawable.material_ic_delete
} else {
Res.drawable.material_ic_add_box
}
),
contentDescription = null,
tint = LocalPallet.current.text100
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,25 @@ package com.flipperdevices.bridge.connection.screens.search

import com.flipperdevices.bridge.connection.config.api.FDevicePersistedStorage
import com.flipperdevices.core.ui.lifecycle.DecomposeViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.StateFlow
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import javax.inject.Inject

abstract class ConnectionSearchViewModel(
private val persistedStorage: FDevicePersistedStorage
class ConnectionSearchViewModel @Inject constructor(
private val persistedStorage: FDevicePersistedStorage,
searchDelegatesFactories: MutableSet<ConnectionSearchDelegate.Factory>
) : DecomposeViewModel() {
abstract fun getDevicesFlow(): StateFlow<ImmutableList<ConnectionSearchItem>>
private val searchDelegates = searchDelegatesFactories.map { it(viewModelScope) }
private val combinedFlow = combine(searchDelegates.map { it.getDevicesFlow() }) { flows ->
flows.toList().flatten().toImmutableList()
}.stateIn(viewModelScope, SharingStarted.Lazily, persistentListOf())

fun getDevicesFlow() = combinedFlow

fun onDeviceClick(searchItem: ConnectionSearchItem) {
viewModelScope.launch {
if (searchItem.isAdded) {
Expand Down
Loading
Loading