Initial step in refactoring RadioInterfaceService for dependency injection

Extracts USB device management into a `UsbRepository`.

In order for `SerialInterface to gain access to this prior to
the `RadioInterfaceService` being fully natively dependency
injected, all `InterfaceFactory` implementations needed
to be modified to accept the `UsbRepository` via argument.  This
will go away in a future PR.

Changed `assumePermission` constant to `false` as it was preventing
the request for permission from occurring, breaking serial connectivity.

Minor improvement: SerialInterface re-bonding by device name is
now supported.
This commit is contained in:
Mike Cumings 2022-04-08 11:34:44 -07:00
parent 26b6081e9c
commit dd41527bbc
17 changed files with 293 additions and 102 deletions

View file

@ -0,0 +1,25 @@
package com.geeksville.mesh.repository.usb
import com.hoho.android.usbserial.driver.CdcAcmSerialDriver
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
import dagger.Reusable
import javax.inject.Inject
import javax.inject.Provider
/**
* Creates a probe table for the USB driver. This augments the default device-to-driver
* mappings with additional known working configurations. See this package's README for
* more info.
*/
@Reusable
class ProbeTableProvider @Inject constructor() : Provider<ProbeTable> {
override fun get(): ProbeTable {
return UsbSerialProber.getDefaultProbeTable().apply {
// RAK 4631:
addProduct(9114, 32809, CdcAcmSerialDriver::class.java)
// LilyGo TBeam v1.1:
addProduct(6790, 21972, CdcAcmSerialDriver::class.java)
}
}
}

View file

@ -0,0 +1,23 @@
# USB Module
This module provides a repository for acessing USB devices.
## Device Support
In order to be picked up, devices need to be supported by two different mechanisms:
- Android needs to be supplied with a device filter so that it knows what devices to inform
the app about. These are expressed as vendor and device IDs in `src/res/xml/device_filter.xml`.
- The USB driver library also needs to have a mapping between the vendor + device IDs and the
driver to use for communications. Many mappings are already natively supported by the driver
but unknown devices can have manual mappings added via `ProbeTableProvider`.
The [Serial USB Terminal](https://play.google.com/store/apps/details?id=de.kai_morich.serial_usb_terminal)
app in the Google Play Store seems to be a good app for determining both the vendor and
device IDs as well as testing different underlying drivers.
## Testing
When granting permissions to a USB device, the Android platform remembers the user's decision.
In order to test the permission granting logic, re-install the app. This will cause Android
to forget previously granted permissions and will re-trigger the permission acquisition logic.

View file

@ -0,0 +1,43 @@
package com.geeksville.mesh.repository.usb
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import com.geeksville.android.Logging
import com.geeksville.util.exceptionReporter
import javax.inject.Inject
/**
* A helper class to call onChanged when bluetooth is enabled or disabled or when permissions are
* changed.
*/
class UsbBroadcastReceiver @Inject constructor(
private val usbRepository: UsbRepository
) : BroadcastReceiver(), Logging {
// Can be used for registering
internal val intentFilter get() = IntentFilter().apply {
addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
}
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
val deviceName: String = intent.getParcelableExtra<UsbDevice?>(UsbManager.EXTRA_DEVICE)?.deviceName ?: "unknown"
when (intent.action) {
UsbManager.ACTION_USB_DEVICE_DETACHED -> {
debug("USB device '$deviceName' was detached")
usbRepository.refreshState()
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
debug("USB device '$deviceName' was attached")
usbRepository.refreshState()
}
UsbManager.EXTRA_PERMISSION_GRANTED -> {
debug("USB device '$deviceName' was granted permission")
usbRepository.refreshState()
}
}
}
}

View file

@ -0,0 +1,77 @@
package com.geeksville.mesh.repository.usb
import android.app.Application
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.android.Logging
import com.geeksville.mesh.CoroutineDispatchers
import com.hoho.android.usbserial.driver.UsbSerialProber
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository responsible for maintaining and updating the state of USB connectivity.
*/
@OptIn(ExperimentalCoroutinesApi::class)
@Singleton
class UsbRepository @Inject constructor(
private val application: Application,
private val dispatchers: CoroutineDispatchers,
private val processLifecycle: Lifecycle,
private val usbBroadcastReceiverLazy: dagger.Lazy<UsbBroadcastReceiver>,
private val usbManagerLazy: dagger.Lazy<UsbManager?>,
private val usbSerialProberLazy: dagger.Lazy<UsbSerialProber>
) : Logging {
private val _serialDevices = MutableStateFlow(emptyMap<String, UsbDevice>())
@Suppress("unused") // Retained as public API
val serialDevices = _serialDevices
.asStateFlow()
val serialDevicesWithDrivers = _serialDevices
.mapLatest { serialDevices ->
val serialProber = usbSerialProberLazy.get()
buildMap {
serialDevices.forEach { (k, v) ->
serialProber.probeDevice(v)?.let { driver ->
put(k, driver)
}
}
}
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
@Suppress("unused") // Retained as public API
val serialDevicesWithPermission = _serialDevices
.mapLatest { serialDevices ->
usbManagerLazy.get()?.let { usbManager ->
serialDevices.filterValues { device ->
usbManager.hasPermission(device)
}
} ?: emptyMap()
}.stateIn(processLifecycle.coroutineScope, SharingStarted.Eagerly, emptyMap())
init {
processLifecycle.coroutineScope.launch(dispatchers.default) {
refreshStateInternal()
usbBroadcastReceiverLazy.get().let { receiver ->
application.registerReceiver(receiver, receiver.intentFilter)
}
}
}
fun refreshState() {
processLifecycle.coroutineScope.launch(dispatchers.default) {
refreshStateInternal()
}
}
private suspend fun refreshStateInternal() = withContext(dispatchers.default) {
_serialDevices.emit(usbManagerLazy.get()?.deviceList ?: emptyMap())
}
}

View file

@ -0,0 +1,27 @@
package com.geeksville.mesh.repository.usb
import android.app.Application
import android.content.Context
import android.hardware.usb.UsbManager
import com.hoho.android.usbserial.driver.ProbeTable
import com.hoho.android.usbserial.driver.UsbSerialProber
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
@Module
@InstallIn(SingletonComponent::class)
interface UsbRepositoryModule {
companion object {
@Provides
fun provideUsbManager(application: Application): UsbManager? =
application.getSystemService(Context.USB_SERVICE) as UsbManager?
@Provides
fun provideProbeTable(provider: ProbeTableProvider): ProbeTable = provider.get()
@Provides
fun provideUsbSerialProber(probeTable: ProbeTable): UsbSerialProber = UsbSerialProber(probeTable)
}
}