kopia lustrzana https://github.com/meshtastic/Meshtastic-Android
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
rodzic
121376201d
commit
a7b0d70c03
|
@ -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 =
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}")
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.geeksville.mesh.repository.radio
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
/**
|
||||
* Factory for creating `BluetoothInterface` instances.
|
||||
*/
|
||||
@AssistedFactory
|
||||
interface BluetoothInterfaceFactory : InterfaceFactorySpi<BluetoothInterface>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.geeksville.mesh.repository.radio
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
/**
|
||||
* Factory for creating `MockInterface` instances.
|
||||
*/
|
||||
@AssistedFactory
|
||||
interface MockInterfaceFactory : InterfaceFactorySpi<MockInterface>
|
|
@ -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)
|
||||
}
|
|
@ -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) {
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.geeksville.mesh.repository.radio
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
/**
|
||||
* Factory for creating `NopInterface` instances.
|
||||
*/
|
||||
@AssistedFactory
|
||||
interface NopInterfaceFactory : InterfaceFactorySpi<NopInterface>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.geeksville.mesh.repository.radio
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
/**
|
||||
* Factory for creating `SerialInterface` instances.
|
||||
*/
|
||||
@AssistedFactory
|
||||
interface SerialInterfaceFactory : InterfaceFactorySpi<SerialInterface>
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.geeksville.mesh.repository.radio
|
||||
|
||||
import dagger.assisted.AssistedFactory
|
||||
|
||||
/**
|
||||
* Factory for creating `TCPInterface` instances.
|
||||
*/
|
||||
@AssistedFactory
|
||||
interface TCPInterfaceFactory : InterfaceFactorySpi<TCPInterface>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue