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