Continued work on #369: Inject radio interface implementations (#481)

This required creation of new interfaces in order to break the
static coupling.  This also allowed for the removal of some plumbing
of dependencies of these implementations since they are now directly
injected.
pull/765/head
Mike Cumings 2023-10-24 12:09:18 -07:00 zatwierdzone przez GitHub
rodzic 121376201d
commit a7b0d70c03
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
25 zmienionych plików z 370 dodań i 220 usunięć

Wyświetl plik

@ -38,7 +38,8 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.primaryChannel
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.repository.radio.BluetoothInterface
import com.geeksville.mesh.repository.radio.SerialInterface
import com.geeksville.mesh.repository.radio.InterfaceId
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.*
import com.geeksville.mesh.ui.*
import com.geeksville.mesh.ui.map.MapFragment
@ -121,6 +122,9 @@ class MainActivity : AppCompatActivity(), Logging {
@Inject
internal lateinit var serviceRepository: ServiceRepository
@Inject
internal lateinit var radioInterfaceService: RadioInterfaceService
private val bluetoothPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result.entries.all { it.value }) {
@ -462,9 +466,9 @@ class MainActivity : AppCompatActivity(), Logging {
try {
usbDevice?.let { usb ->
debug("Switching to USB radio ${usb.deviceName}")
service.setDeviceAddress(SerialInterface.toInterfaceName(usb.deviceName))
usbDevice =
null // Only switch once - thereafter it should be stored in settings
val address = radioInterfaceService.toInterfaceAddress(InterfaceId.SERIAL, usb.deviceName)
service.setDeviceAddress(address)
usbDevice = null // Only switch once - thereafter it should be stored in settings
}
val connectionState =

Wyświetl plik

@ -20,9 +20,8 @@ import com.geeksville.mesh.R
import com.geeksville.mesh.android.*
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.nsd.NsdRepository
import com.geeksville.mesh.repository.radio.MockInterface
import com.geeksville.mesh.repository.radio.InterfaceId
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.SerialInterface
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.util.anonymize
import com.hoho.android.usbserial.driver.UsbSerialDriver
@ -83,10 +82,14 @@ class BTScanModel @Inject constructor(
device.bondState == BluetoothDevice.BOND_BONDED
)
class USBDeviceListEntry(usbManager: UsbManager, val usb: UsbSerialDriver) : DeviceListEntry(
class USBDeviceListEntry(
radioInterfaceService: RadioInterfaceService,
usbManager: UsbManager,
val usb: UsbSerialDriver,
) : DeviceListEntry(
usb.device.deviceName,
SerialInterface.toInterfaceName(usb.device.deviceName),
SerialInterface.assumePermission || usbManager.hasPermission(usb.device)
radioInterfaceService.toInterfaceAddress(InterfaceId.SERIAL, usb.device.deviceName),
usbManager.hasPermission(usb.device),
)
class TCPDeviceListEntry(val service: NsdServiceInfo) : DeviceListEntry(
@ -177,7 +180,7 @@ class BTScanModel @Inject constructor(
private fun setupScan(): Boolean {
selectedAddress = radioInterfaceService.getDeviceAddress()
return if (MockInterface.addressValid(context, usbRepository, "")) {
return if (radioInterfaceService.isAddressValid(radioInterfaceService.mockInterfaceAddress)) {
warn("Running under emulator/test lab")
val testnodes = listOf(
@ -215,7 +218,7 @@ class BTScanModel @Inject constructor(
}
usbDevices.value?.forEach { (_, d) ->
addDevice(USBDeviceListEntry(context.usbManager, d))
addDevice(USBDeviceListEntry(radioInterfaceService, context.usbManager, d))
}
devices.value = newDevs

Wyświetl plik

@ -1,16 +1,17 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
import android.content.Context
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.bluetoothManager
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.service.*
import com.geeksville.mesh.util.anonymize
import com.geeksville.mesh.util.exceptionReporter
import com.geeksville.mesh.util.ignoreException
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@ -76,25 +77,15 @@ A variable keepAllPackets, if set to true will suppress this behavior and instea
* Note - this class intentionally dumb. It doesn't understand protobuf framing etc...
* It is designed to be simple so it can be stubbed out with a simulated version as needed.
*/
class BluetoothInterface(
val context: Context,
val service: RadioInterfaceService,
val address: String) : IRadioInterface,
Logging {
class BluetoothInterface @AssistedInject constructor(
context: Application,
bluetoothAdapter: dagger.Lazy<BluetoothAdapter?>,
private val service: RadioInterfaceService,
@Assisted val address: String,
) : IRadioInterface, Logging {
companion object : Logging, InterfaceFactory('x') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = BluetoothInterface(context, service, rest)
init {
registerFactory()
}
/// this service UUID is publically visible for scanning
companion object {
/// this service UUID is publicly visible for scanning
val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
var invalidVersion = false
@ -108,22 +99,6 @@ class BluetoothInterface(
val BTM_FROMNUM_CHARACTER: UUID =
UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(
context: Context,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): Boolean {
/// Get our bluetooth adapter (should always succeed except on emulator
val allPaired = context.bluetoothManager?.adapter?.bondedDevices.orEmpty()
.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
false
} else
true
}
/**
* this is created in onCreate()
* We do an ugly hack of keeping it in the singleton so we can share it for the rare software update case
@ -161,7 +136,7 @@ class BluetoothInterface(
init {
// Note: this call does no comms, it just creates the device object (even if the
// device is off/not connected)
val device = context.bluetoothManager?.adapter?.getRemoteDevice(address)
val device = bluetoothAdapter.get()?.getRemoteDevice(address)
if (device != null) {
info("Creating radio interface service. device=${address.anonymize}")

Wyświetl plik

@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `BluetoothInterface` instances.
*/
@AssistedFactory
interface BluetoothInterfaceFactory : InterfaceFactorySpi<BluetoothInterface>

Wyświetl plik

@ -0,0 +1,32 @@
package com.geeksville.mesh.repository.radio
import android.annotation.SuppressLint
import android.bluetooth.BluetoothAdapter
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.util.anonymize
import javax.inject.Inject
/**
* Bluetooth backend implementation.
*/
class BluetoothInterfaceSpec @Inject constructor(
private val factory: BluetoothInterfaceFactory,
private val bluetoothAdapter: dagger.Lazy<BluetoothAdapter?>
): InterfaceSpec<BluetoothInterface>, Logging {
override fun createInterface(rest: String): BluetoothInterface {
return factory.create(rest)
}
/** Return true if this address is still acceptable. For BLE that means, still bonded */
@SuppressLint("MissingPermission")
override fun addressValid(rest: String): Boolean {
val allPaired = bluetoothAdapter.get()?.bondedDevices.orEmpty()
.map { it.address }.toSet()
return if (!allPaired.contains(rest)) {
warn("Ignoring stale bond to ${rest.anonymize}")
false
} else
true
}
}

Wyświetl plik

@ -1,24 +1,41 @@
package com.geeksville.mesh.repository.radio
import android.content.Context
import com.geeksville.mesh.repository.usb.UsbRepository
import javax.inject.Inject
import javax.inject.Provider
/**
* A base class for the singleton factories that make interfaces. One instance per interface type
* Entry point for create radio backend instances given a specific address.
*
* This class is responsible for building and dissecting radio addresses based upon
* their interface type and the "rest" of the address (which varies per implementation).
*/
abstract class InterfaceFactory(val prefix: Char) {
companion object {
private val factories = mutableMapOf<Char, InterfaceFactory>()
fun getFactory(l: Char) = factories.get(l)
class InterfaceFactory @Inject constructor(
private val nopInterfaceFactory: NopInterfaceFactory,
private val specMap: Map<InterfaceId, @JvmSuppressWildcards Provider<InterfaceSpec<*>>>
) {
internal val nopInterface by lazy {
nopInterfaceFactory.create("")
}
protected fun registerFactory() {
factories[prefix] = this
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String {
return "${interfaceId.id}$rest"
}
abstract fun createInterface(context: Context, service: RadioInterfaceService, usbRepository: UsbRepository, rest: String): IRadioInterface
fun createInterface(address: String): IRadioInterface {
val (spec, rest) = splitAddress(address)
return spec?.createInterface(rest) ?: nopInterface
}
/** Return true if this address is still acceptable. For BLE that means, still bonded */
open fun addressValid(context: Context, usbRepository: UsbRepository, rest: String): Boolean = true
fun addressValid(address: String?): Boolean {
return address?.let {
val (spec, rest) = splitAddress(it)
spec?.addressValid(rest)
} ?: false
}
private fun splitAddress(address: String): Pair<InterfaceSpec<*>?, String> {
val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() }
val rest = address.substring(1)
return Pair(c, rest)
}
}

Wyświetl plik

@ -0,0 +1,14 @@
package com.geeksville.mesh.repository.radio
/**
* Radio interface factory service provider interface. Each radio backend implementation needs
* to have a factory to create new instances. These instances are specific to a particular
* address. This interface defines a common API across all radio interfaces for obtaining
* implementation instances.
*
* This is primarily used in conjunction with Dagger assisted injection for each backend
* interface type.
*/
interface InterfaceFactorySpi<T: IRadioInterface> {
fun create(rest: String): T
}

Wyświetl plik

@ -0,0 +1,18 @@
package com.geeksville.mesh.repository.radio
/**
* Address identifiers for all supported radio backend implementations.
*/
enum class InterfaceId(val id: Char) {
BLUETOOTH('x'),
MOCK('m'),
NOP('n'),
SERIAL('s'),
TCP('t');
companion object {
fun forIdChar(id: Char): InterfaceId? {
return values().firstOrNull { it.id == id }
}
}
}

Wyświetl plik

@ -0,0 +1,11 @@
package com.geeksville.mesh.repository.radio
import dagger.MapKey
/**
* Dagger `MapKey` implementation allowing for `InterfaceId` to be used as a map key.
*/
@MapKey
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_SETTER, AnnotationTarget.PROPERTY_GETTER)
@Retention(AnnotationRetention.RUNTIME)
annotation class InterfaceMapKey(val value: InterfaceId)

Wyświetl plik

@ -0,0 +1,11 @@
package com.geeksville.mesh.repository.radio
/**
* This interface defines the contract that all radio backend implementations must adhere to.
*/
interface InterfaceSpec<T : IRadioInterface> {
fun createInterface(rest: String): T
/** Return true if this address is still acceptable. For BLE that means, still bonded */
fun addressValid(rest: String): Boolean = true
}

Wyświetl plik

@ -1,36 +1,17 @@
package com.geeksville.mesh.repository.radio
import android.content.Context
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.*
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.usb.UsbRepository
import com.google.protobuf.ByteString
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
/** A simulated interface that is used for testing in the simulator */
class MockInterface(private val service: RadioInterfaceService) : Logging, IRadioInterface {
companion object : Logging, InterfaceFactory('m') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = MockInterface(service)
override fun addressValid(
context: Context,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): Boolean =
BuildUtils.isEmulator || ((context.applicationContext as GeeksvilleApplication).isInTestLab)
init {
registerFactory()
}
}
class MockInterface @AssistedInject constructor(
private val service: RadioInterfaceService,
@Assisted val address: String
) : Logging, IRadioInterface {
private var messageCount = 50
// an infinite sequence of ints

Wyświetl plik

@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `MockInterface` instances.
*/
@AssistedFactory
interface MockInterfaceFactory : InterfaceFactorySpi<MockInterface>

Wyświetl plik

@ -0,0 +1,22 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
import com.geeksville.mesh.android.BuildUtils
import com.geeksville.mesh.android.GeeksvilleApplication
import javax.inject.Inject
/**
* Mock interface backend implementation.
*/
class MockInterfaceSpec @Inject constructor(
private val application: Application,
private val factory: MockInterfaceFactory
): InterfaceSpec<MockInterface> {
override fun createInterface(rest: String): MockInterface {
return factory.create(rest)
}
/** Return true if this address is still acceptable. For BLE that means, still bonded */
override fun addressValid(rest: String): Boolean =
BuildUtils.isEmulator || ((application as GeeksvilleApplication).isInTestLab)
}

Wyświetl plik

@ -1,23 +1,9 @@
package com.geeksville.mesh.repository.radio
import android.content.Context
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.repository.usb.UsbRepository
class NopInterface : IRadioInterface {
companion object : Logging, InterfaceFactory('n') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = NopInterface()
init {
registerFactory()
}
}
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
class NopInterface @AssistedInject constructor(@Assisted val address: String) : IRadioInterface {
override fun handleSendToRadio(p: ByteArray) {
}

Wyświetl plik

@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `NopInterface` instances.
*/
@AssistedFactory
interface NopInterfaceFactory : InterfaceFactorySpi<NopInterface>

Wyświetl plik

@ -0,0 +1,14 @@
package com.geeksville.mesh.repository.radio
import javax.inject.Inject
/**
* No-op interface backend implementation.
*/
class NopInterfaceSpec @Inject constructor(
private val factory: NopInterfaceFactory
): InterfaceSpec<NopInterface> {
override fun createInterface(rest: String): NopInterface {
return factory.create(rest)
}
}

Wyświetl plik

@ -6,14 +6,13 @@ import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.android.BinaryLogFile
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.CoroutineDispatchers
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
import com.geeksville.mesh.repository.nsd.NsdRepository
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.util.anonymize
import com.geeksville.mesh.util.ignoreException
import com.geeksville.mesh.util.toRemoteExceptions
@ -45,8 +44,9 @@ class RadioInterfaceService @Inject constructor(
bluetoothRepository: BluetoothRepository,
nsdRepository: NsdRepository,
private val processLifecycle: Lifecycle,
private val usbRepository: UsbRepository,
@RadioRepositoryQualifier private val prefs: SharedPreferences
@RadioRepositoryQualifier private val prefs: SharedPreferences,
private val interfaceFactory: InterfaceFactory,
private val mockInterfaceSpec: MockInterfaceSpec
) : Logging {
private val _connectionState = MutableStateFlow(RadioServiceConnectionState())
@ -72,12 +72,16 @@ class RadioInterfaceService @Inject constructor(
private lateinit var sentPacketsLog: BinaryLogFile // inited in onCreate
private lateinit var receivedPacketsLog: BinaryLogFile
val mockInterfaceAddress: String by lazy {
toInterfaceAddress(InterfaceId.MOCK, "")
}
/**
* We recreate this scope each time we stop an interface
*/
var serviceScope = CoroutineScope(Dispatchers.IO + Job())
private var radioIf: IRadioInterface = NopInterface()
private var radioIf: IRadioInterface = NopInterface("")
/** true if we have started our interface
*
@ -102,18 +106,17 @@ class RadioInterfaceService @Inject constructor(
companion object : Logging {
const val DEVADDR_KEY = "devAddr2" // the new name for devaddr
}
init {
/// We keep this var alive so that the following factory objects get created and not stripped during the android build
val factories = arrayOf<InterfaceFactory>(
BluetoothInterface,
SerialInterface,
TCPInterface,
MockInterface,
NopInterface
)
info("Using ${factories.size} interface factories")
}
/**
* Constructs a full radio address for the specific interface type.
*/
fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String {
return interfaceFactory.toInterfaceAddress(interfaceId, rest)
}
fun isAddressValid(address: String?): Boolean {
return interfaceFactory.addressValid(address)
}
/** Return the device we are configured to use, or null for none
@ -129,8 +132,8 @@ class RadioInterfaceService @Inject constructor(
var address = prefs.getString(DEVADDR_KEY, null)
// If we are running on the emulator we default to the mock interface, so we can have some data to show to the user
if (address == null && MockInterface.addressValid(context, usbRepository, ""))
address = MockInterface.prefix.toString()
if (address == null && mockInterfaceSpec.addressValid(""))
address = "${InterfaceId.MOCK.id}"
return address
}
@ -146,17 +149,11 @@ class RadioInterfaceService @Inject constructor(
fun getBondedDeviceAddress(): String? {
// If the user has unpaired our device, treat things as if we don't have one
val address = getDeviceAddress()
/// Interfaces can filter addresses to indicate that address is no longer acceptable
if (address != null) {
val c = address[0]
val rest = address.substring(1)
val isValid = InterfaceFactory.getFactory(c)
?.addressValid(context, usbRepository, rest) ?: false
if (!isValid)
return null
return if (interfaceFactory.addressValid(address)) {
address
} else {
null
}
return address
}
private fun broadcastConnectionChanged(isConnected: Boolean, isPermanent: Boolean) {
@ -219,10 +216,7 @@ class RadioInterfaceService @Inject constructor(
if (logReceives)
receivedPacketsLog = BinaryLogFile(context, "receive_log.pb")
val c = address[0]
val rest = address.substring(1)
radioIf =
InterfaceFactory.getFactory(c)?.createInterface(context, this, usbRepository, rest) ?: NopInterface()
radioIf = interfaceFactory.createInterface(address)
}
}
}
@ -231,7 +225,7 @@ class RadioInterfaceService @Inject constructor(
val r = radioIf
info("stopping interface $r")
isStarted = false
radioIf = NopInterface()
radioIf = interfaceFactory.nopInterface
r.close()
// cancel any old jobs and get ready for the new ones

Wyświetl plik

@ -3,17 +3,42 @@ package com.geeksville.mesh.repository.radio
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoMap
import dagger.multibindings.Multibinds
@Suppress("unused") // Used by hilt
@Module
@InstallIn(SingletonComponent::class)
object RadioRepositoryModule {
@Provides
@RadioRepositoryQualifier
fun provideSharedPreferences(application: Application): SharedPreferences {
return application.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE)
abstract class RadioRepositoryModule {
@Multibinds
abstract fun interfaceMap(): Map<InterfaceId, @JvmSuppressWildcards InterfaceSpec<*>>
@[Binds IntoMap InterfaceMapKey(InterfaceId.BLUETOOTH)]
abstract fun bindBluetoothInterfaceSpec(spec: BluetoothInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.MOCK)]
abstract fun bindMockInterfaceSpec(spec: MockInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.NOP)]
abstract fun bindNopInterfaceSpec(spec: NopInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.SERIAL)]
abstract fun bindSerialInterfaceSpec(spec: SerialInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
@[Binds IntoMap InterfaceMapKey(InterfaceId.TCP)]
abstract fun bindTCPInterfaceSpec(spec: TCPInterfaceSpec): @JvmSuppressWildcards InterfaceSpec<*>
companion object {
@Provides
@RadioRepositoryQualifier
fun provideSharedPreferences(application: Application): SharedPreferences {
return application.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE)
}
}
}

Wyświetl plik

@ -1,67 +1,22 @@
package com.geeksville.mesh.repository.radio
import android.content.Context
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.usbManager
import com.geeksville.mesh.repository.usb.SerialConnection
import com.geeksville.mesh.repository.usb.SerialConnectionListener
import com.geeksville.mesh.repository.usb.UsbRepository
import com.hoho.android.usbserial.driver.UsbSerialDriver
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.atomic.AtomicReference
/**
* An interface that assumes we are talking to a meshtastic device via USB serial
*/
class SerialInterface(
class SerialInterface @AssistedInject constructor(
service: RadioInterfaceService,
private val serialInterfaceSpec: SerialInterfaceSpec,
private val usbRepository: UsbRepository,
private val address: String) :
StreamInterface(service), Logging {
companion object : Logging, InterfaceFactory('s') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository,
rest: String
): IRadioInterface = SerialInterface(service, usbRepository, rest)
init {
registerFactory()
}
/**
* according to https://stackoverflow.com/questions/12388914/usb-device-access-pop-up-suppression/15151075#15151075
* we should never ask for USB permissions ourselves, instead we should rely on the external dialog printed by the system. If
* we do that the system will remember we have accesss
*/
const val assumePermission = false
fun toInterfaceName(deviceName: String) = "s$deviceName"
override fun addressValid(
context: Context,
usbRepository: UsbRepository,
rest: String
): Boolean {
usbRepository.serialDevicesWithDrivers.value.filterValues {
assumePermission || context.usbManager.hasPermission(it.device)
}
findSerial(usbRepository, rest)?.let { d ->
return assumePermission || context.usbManager.hasPermission(d.device)
}
return false
}
private fun findSerial(usbRepository: UsbRepository, rest: String): UsbSerialDriver? {
val deviceMap = usbRepository.serialDevicesWithDrivers.value
return if (deviceMap.containsKey(rest)) {
deviceMap[rest]!!
} else {
deviceMap.map { (_, driver) -> driver }.firstOrNull()
}
}
}
@Assisted private val address: String,
) : StreamInterface(service), Logging {
private var connRef = AtomicReference<SerialConnection?>()
init {
@ -74,7 +29,7 @@ class SerialInterface(
}
override fun connect() {
val device = findSerial(usbRepository, address)
val device = serialInterfaceSpec.findSerial(address)
if (device == null) {
errormsg("Can't find device")
} else {

Wyświetl plik

@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `SerialInterface` instances.
*/
@AssistedFactory
interface SerialInterfaceFactory : InterfaceFactorySpi<SerialInterface>

Wyświetl plik

@ -0,0 +1,41 @@
package com.geeksville.mesh.repository.radio
import android.app.Application
import com.geeksville.mesh.android.usbManager
import com.geeksville.mesh.repository.usb.UsbRepository
import com.hoho.android.usbserial.driver.UsbSerialDriver
import javax.inject.Inject
/**
* Serial/USB interface backend implementation.
*/
class SerialInterfaceSpec @Inject constructor(
private val factory: SerialInterfaceFactory,
private val context: Application,
private val usbRepository: UsbRepository,
): InterfaceSpec<SerialInterface> {
override fun createInterface(rest: String): SerialInterface {
return factory.create(rest)
}
override fun addressValid(
rest: String
): Boolean {
usbRepository.serialDevicesWithDrivers.value.filterValues {
context.usbManager.hasPermission(it.device)
}
findSerial(rest)?.let { d ->
return context.usbManager.hasPermission(d.device)
}
return false
}
internal fun findSerial(rest: String): UsbSerialDriver? {
val deviceMap = usbRepository.serialDevicesWithDrivers.value
return if (deviceMap.containsKey(rest)) {
deviceMap[rest]!!
} else {
deviceMap.map { (_, driver) -> driver }.firstOrNull()
}
}
}

Wyświetl plik

@ -1,10 +1,10 @@
package com.geeksville.mesh.repository.radio
import android.content.Context
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.util.Exceptions
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
@ -16,21 +16,12 @@ import java.net.InetAddress
import java.net.Socket
import java.net.SocketTimeoutException
class TCPInterface(service: RadioInterfaceService, private val address: String) :
StreamInterface(service) {
companion object : Logging, InterfaceFactory('t') {
override fun createInterface(
context: Context,
service: RadioInterfaceService,
usbRepository: UsbRepository, // Temporary until dependency injection transition is completed
rest: String
): IRadioInterface = TCPInterface(service, rest)
init {
registerFactory()
}
class TCPInterface @AssistedInject constructor(
service: RadioInterfaceService,
@Assisted private val address: String,
) : StreamInterface(service), Logging {
companion object {
const val MAX_RETRIES_ALLOWED = Int.MAX_VALUE
const val MIN_BACKOFF_MILLIS = 1 * 1000L // 1 second
const val MAX_BACKOFF_MILLIS = 5 * 60 * 1000L // 5 minutes

Wyświetl plik

@ -0,0 +1,9 @@
package com.geeksville.mesh.repository.radio
import dagger.assisted.AssistedFactory
/**
* Factory for creating `TCPInterface` instances.
*/
@AssistedFactory
interface TCPInterfaceFactory : InterfaceFactorySpi<TCPInterface>

Wyświetl plik

@ -0,0 +1,14 @@
package com.geeksville.mesh.repository.radio
import javax.inject.Inject
/**
* TCP interface backend implementation.
*/
class TCPInterfaceSpec @Inject constructor(
private val factory: TCPInterfaceFactory
): InterfaceSpec<TCPInterface> {
override fun createInterface(rest: String): TCPInterface {
return factory.create(rest)
}
}

Wyświetl plik

@ -29,13 +29,10 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.asLiveData
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.hideKeyboard
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.ConfigProtos
import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.ModuleConfigProtos
import com.geeksville.mesh.android.*
import com.geeksville.mesh.databinding.SettingsFragmentBinding
@ -44,8 +41,7 @@ import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getInitials
import com.geeksville.mesh.repository.location.LocationRepository
import com.geeksville.mesh.repository.radio.MockInterface
import com.geeksville.mesh.repository.usb.UsbRepository
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.SoftwareUpdateService
import com.geeksville.mesh.util.PendingIntentCompat
@ -71,7 +67,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
private val model: UIViewModel by activityViewModels()
@Inject
internal lateinit var usbRepository: UsbRepository
internal lateinit var radioInterfaceServiceLazy: dagger.Lazy<RadioInterfaceService>
@Inject
internal lateinit var locationRepository: LocationRepository
@ -506,7 +502,8 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
// If we are running on an emulator, always leave this message showing so we can test the worst case layout
val curRadio = scanModel.selectedAddress
if (curRadio != null && !MockInterface.addressValid(requireContext(), usbRepository, "")) {
val radioInterfaceService = radioInterfaceServiceLazy.get()
if (curRadio != null && !radioInterfaceService.isAddressValid(radioInterfaceService.mockInterfaceAddress)) {
binding.warningNotPaired.visibility = View.GONE
} else if (bluetoothViewModel.enabled.value == true) {
binding.warningNotPaired.visibility = View.VISIBLE