From a7b0d70c0381a873bb3795e6deb9db06f804d62a Mon Sep 17 00:00:00 2001 From: Mike Cumings Date: Tue, 24 Oct 2023 12:09:18 -0700 Subject: [PATCH] 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. --- .../java/com/geeksville/mesh/MainActivity.kt | 12 ++-- .../com/geeksville/mesh/model/BTScanModel.kt | 17 +++--- .../repository/radio/BluetoothInterface.kt | 51 ++++------------ .../radio/BluetoothInterfaceFactory.kt | 9 +++ .../radio/BluetoothInterfaceSpec.kt | 32 ++++++++++ .../mesh/repository/radio/InterfaceFactory.kt | 43 +++++++++---- .../repository/radio/InterfaceFactorySpi.kt | 14 +++++ .../mesh/repository/radio/InterfaceId.kt | 18 ++++++ .../mesh/repository/radio/InterfaceMapKey.kt | 11 ++++ .../mesh/repository/radio/InterfaceSpec.kt | 11 ++++ .../mesh/repository/radio/MockInterface.kt | 33 +++------- .../repository/radio/MockInterfaceFactory.kt | 9 +++ .../repository/radio/MockInterfaceSpec.kt | 22 +++++++ .../mesh/repository/radio/NopInterface.kt | 20 +------ .../repository/radio/NopInterfaceFactory.kt | 9 +++ .../mesh/repository/radio/NopInterfaceSpec.kt | 14 +++++ .../repository/radio/RadioInterfaceService.kt | 60 +++++++++---------- .../repository/radio/RadioRepositoryModule.kt | 35 +++++++++-- .../mesh/repository/radio/SerialInterface.kt | 59 +++--------------- .../radio/SerialInterfaceFactory.kt | 9 +++ .../repository/radio/SerialInterfaceSpec.kt | 41 +++++++++++++ .../mesh/repository/radio/TCPInterface.kt | 23 +++---- .../repository/radio/TCPInterfaceFactory.kt | 9 +++ .../mesh/repository/radio/TCPInterfaceSpec.kt | 14 +++++ .../geeksville/mesh/ui/SettingsFragment.kt | 15 ++--- 25 files changed, 370 insertions(+), 220 deletions(-) create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceFactory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactorySpi.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceSpec.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceFactory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceSpec.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceFactory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceSpec.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceFactory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceSpec.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceFactory.kt create mode 100644 app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceSpec.kt diff --git a/app/src/main/java/com/geeksville/mesh/MainActivity.kt b/app/src/main/java/com/geeksville/mesh/MainActivity.kt index 61714057d..7eac6b4f9 100644 --- a/app/src/main/java/com/geeksville/mesh/MainActivity.kt +++ b/app/src/main/java/com/geeksville/mesh/MainActivity.kt @@ -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 = diff --git a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt index e00d9cea3..8cec2acbd 100644 --- a/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/BTScanModel.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt index bd0887f3a..d43542c7f 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterface.kt @@ -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, + 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}") diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceFactory.kt new file mode 100644 index 000000000..8c0ce5912 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceFactory.kt @@ -0,0 +1,9 @@ +package com.geeksville.mesh.repository.radio + +import dagger.assisted.AssistedFactory + +/** + * Factory for creating `BluetoothInterface` instances. + */ +@AssistedFactory +interface BluetoothInterfaceFactory : InterfaceFactorySpi \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt new file mode 100644 index 000000000..2b6a16b5f --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/BluetoothInterfaceSpec.kt @@ -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 + +): InterfaceSpec, 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt index ce80b1409..9faf8e07c 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactory.kt @@ -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() - - fun getFactory(l: Char) = factories.get(l) +class InterfaceFactory @Inject constructor( + private val nopInterfaceFactory: NopInterfaceFactory, + private val specMap: Map>> +) { + 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?, String> { + val c = address[0].let { InterfaceId.forIdChar(it) }?.let { specMap[it]?.get() } + val rest = address.substring(1) + return Pair(c, rest) + } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactorySpi.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactorySpi.kt new file mode 100644 index 000000000..69e673b82 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceFactorySpi.kt @@ -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 { + fun create(rest: String): T +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt new file mode 100644 index 000000000..19eb75449 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceId.kt @@ -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 } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt new file mode 100644 index 000000000..c8cd5d1be --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceMapKey.kt @@ -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) diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceSpec.kt new file mode 100644 index 000000000..91bc8b296 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/InterfaceSpec.kt @@ -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 { + 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 +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt index 7747b4109..f09fbaaa6 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterface.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceFactory.kt new file mode 100644 index 000000000..0ad9c6317 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceFactory.kt @@ -0,0 +1,9 @@ +package com.geeksville.mesh.repository.radio + +import dagger.assisted.AssistedFactory + +/** + * Factory for creating `MockInterface` instances. + */ +@AssistedFactory +interface MockInterfaceFactory : InterfaceFactorySpi \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceSpec.kt new file mode 100644 index 000000000..719955777 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/MockInterfaceSpec.kt @@ -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 { + 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) +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterface.kt index 843018208..a9be7cee1 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterface.kt @@ -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) { } diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceFactory.kt new file mode 100644 index 000000000..07ef9cdf7 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceFactory.kt @@ -0,0 +1,9 @@ +package com.geeksville.mesh.repository.radio + +import dagger.assisted.AssistedFactory + +/** + * Factory for creating `NopInterface` instances. + */ +@AssistedFactory +interface NopInterfaceFactory : InterfaceFactorySpi \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceSpec.kt new file mode 100644 index 000000000..50de0f61d --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/NopInterfaceSpec.kt @@ -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 { + override fun createInterface(rest: String): NopInterface { + return factory.create(rest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt index e2d0919dc..e0b9efa56 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioInterfaceService.kt @@ -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( - 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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt index 611e39f1f..ba1821739 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/RadioRepositoryModule.kt @@ -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> + + @[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) + } } } \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt index 58f8b6450..516a66afe 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterface.kt @@ -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() 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 { diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceFactory.kt new file mode 100644 index 000000000..076c09b21 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceFactory.kt @@ -0,0 +1,9 @@ +package com.geeksville.mesh.repository.radio + +import dagger.assisted.AssistedFactory + +/** + * Factory for creating `SerialInterface` instances. + */ +@AssistedFactory +interface SerialInterfaceFactory : InterfaceFactorySpi \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceSpec.kt new file mode 100644 index 000000000..0eb28a3ba --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/SerialInterfaceSpec.kt @@ -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 { + 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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt index c7909c87b..bfe56d010 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterface.kt @@ -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 diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceFactory.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceFactory.kt new file mode 100644 index 000000000..ff359d1fe --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceFactory.kt @@ -0,0 +1,9 @@ +package com.geeksville.mesh.repository.radio + +import dagger.assisted.AssistedFactory + +/** + * Factory for creating `TCPInterface` instances. + */ +@AssistedFactory +interface TCPInterfaceFactory : InterfaceFactorySpi \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceSpec.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceSpec.kt new file mode 100644 index 000000000..f92e3def8 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/TCPInterfaceSpec.kt @@ -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 { + override fun createInterface(rest: String): TCPInterface { + return factory.create(rest) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt index 02e43996b..bc5888ef5 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt @@ -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 @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