2024-11-26 08:38:12 -03:00
|
|
|
/*
|
2025-01-02 06:50:26 -03:00
|
|
|
* Copyright (c) 2025 Meshtastic LLC
|
2024-11-26 08:38:12 -03:00
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
|
* (at your option) any later version.
|
|
|
|
|
*
|
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
|
*
|
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
2022-06-12 16:32:06 -03:00
|
|
|
package com.geeksville.mesh.model
|
|
|
|
|
|
|
|
|
|
import android.annotation.SuppressLint
|
|
|
|
|
import android.app.Application
|
|
|
|
|
import android.bluetooth.BluetoothDevice
|
2024-08-03 07:53:59 -03:00
|
|
|
import android.content.Context
|
2025-07-03 13:26:59 +02:00
|
|
|
import android.content.SharedPreferences
|
2022-06-12 16:32:06 -03:00
|
|
|
import android.hardware.usb.UsbManager
|
2023-11-17 08:46:54 -03:00
|
|
|
import android.os.RemoteException
|
2025-07-05 22:32:03 +00:00
|
|
|
import androidx.core.content.edit
|
2022-06-12 16:32:06 -03:00
|
|
|
import androidx.lifecycle.MutableLiveData
|
|
|
|
|
import androidx.lifecycle.ViewModel
|
2022-12-24 00:20:54 -03:00
|
|
|
import androidx.lifecycle.viewModelScope
|
2022-06-12 16:32:06 -03:00
|
|
|
import com.geeksville.mesh.R
|
2025-05-17 11:39:53 -05:00
|
|
|
import com.geeksville.mesh.android.Logging
|
2022-06-12 16:32:06 -03:00
|
|
|
import com.geeksville.mesh.repository.bluetooth.BluetoothRepository
|
2023-11-17 08:46:54 -03:00
|
|
|
import com.geeksville.mesh.repository.network.NetworkRepository
|
2025-03-17 20:13:27 -03:00
|
|
|
import com.geeksville.mesh.repository.network.NetworkRepository.Companion.toAddressString
|
2023-10-24 12:09:18 -07:00
|
|
|
import com.geeksville.mesh.repository.radio.InterfaceId
|
2022-06-12 16:32:06 -03:00
|
|
|
import com.geeksville.mesh.repository.radio.RadioInterfaceService
|
|
|
|
|
import com.geeksville.mesh.repository.usb.UsbRepository
|
2023-11-17 08:46:54 -03:00
|
|
|
import com.geeksville.mesh.service.MeshService
|
|
|
|
|
import com.geeksville.mesh.service.ServiceRepository
|
2022-09-04 22:52:40 -03:00
|
|
|
import com.geeksville.mesh.util.anonymize
|
2022-06-12 16:32:06 -03:00
|
|
|
import com.hoho.android.usbserial.driver.UsbSerialDriver
|
|
|
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
|
|
|
import kotlinx.coroutines.Job
|
2024-10-02 19:58:11 -03:00
|
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
2025-06-23 20:13:09 +00:00
|
|
|
import kotlinx.coroutines.flow.SharingStarted
|
2025-05-17 11:39:53 -05:00
|
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
|
|
|
import kotlinx.coroutines.flow.asStateFlow
|
2023-11-17 08:46:54 -03:00
|
|
|
import kotlinx.coroutines.flow.catch
|
2023-04-06 21:04:03 -03:00
|
|
|
import kotlinx.coroutines.flow.combine
|
2022-06-12 16:32:06 -03:00
|
|
|
import kotlinx.coroutines.flow.launchIn
|
2025-06-13 06:34:44 -05:00
|
|
|
import kotlinx.coroutines.flow.map
|
2022-06-12 16:32:06 -03:00
|
|
|
import kotlinx.coroutines.flow.onEach
|
2025-06-13 06:34:44 -05:00
|
|
|
import kotlinx.coroutines.flow.stateIn
|
2025-07-03 13:26:59 +02:00
|
|
|
import org.json.JSONArray
|
2025-07-20 13:36:47 -05:00
|
|
|
import org.json.JSONObject
|
2025-07-05 22:32:03 +00:00
|
|
|
import javax.inject.Inject
|
2022-06-12 16:32:06 -03:00
|
|
|
|
|
|
|
|
@HiltViewModel
|
2025-07-03 13:26:59 +02:00
|
|
|
@Suppress("LongParameterList", "TooManyFunctions")
|
2022-06-12 16:32:06 -03:00
|
|
|
class BTScanModel @Inject constructor(
|
|
|
|
|
private val application: Application,
|
2023-11-17 08:46:54 -03:00
|
|
|
private val serviceRepository: ServiceRepository,
|
2022-06-12 16:32:06 -03:00
|
|
|
private val bluetoothRepository: BluetoothRepository,
|
|
|
|
|
private val usbRepository: UsbRepository,
|
2023-11-17 08:46:54 -03:00
|
|
|
private val usbManagerLazy: dagger.Lazy<UsbManager>,
|
|
|
|
|
private val networkRepository: NetworkRepository,
|
2022-06-12 16:32:06 -03:00
|
|
|
private val radioInterfaceService: RadioInterfaceService,
|
2025-07-03 13:26:59 +02:00
|
|
|
private val preferences: SharedPreferences,
|
2022-06-12 16:32:06 -03:00
|
|
|
) : ViewModel(), Logging {
|
|
|
|
|
|
|
|
|
|
private val context: Context get() = application.applicationContext
|
2023-04-06 19:08:19 -03:00
|
|
|
val devices = MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf())
|
2024-06-03 10:17:20 -03:00
|
|
|
val errorText = MutableLiveData<String?>(null)
|
2022-06-12 16:32:06 -03:00
|
|
|
|
2025-07-03 13:26:59 +02:00
|
|
|
private val recentIpAddresses = MutableStateFlow(getRecentAddresses())
|
|
|
|
|
|
2025-06-29 14:18:14 +00:00
|
|
|
private val showMockInterface: StateFlow<Boolean>
|
|
|
|
|
get() =
|
|
|
|
|
MutableStateFlow(radioInterfaceService.isMockInterface()).asStateFlow()
|
2023-11-03 19:01:19 -03:00
|
|
|
|
2022-06-12 16:32:06 -03:00
|
|
|
init {
|
2023-04-06 21:04:03 -03:00
|
|
|
combine(
|
2023-06-29 21:29:38 -03:00
|
|
|
bluetoothRepository.state,
|
2023-11-17 08:46:54 -03:00
|
|
|
networkRepository.resolvedList,
|
2025-07-03 13:26:59 +02:00
|
|
|
recentIpAddresses.asStateFlow(),
|
2024-10-02 19:58:11 -03:00
|
|
|
usbRepository.serialDevicesWithDrivers,
|
|
|
|
|
showMockInterface,
|
2025-07-03 13:26:59 +02:00
|
|
|
) { ble, tcp, recent, usb, showMockInterface ->
|
2023-11-17 08:46:54 -03:00
|
|
|
devices.value = mutableMapOf<String, DeviceListEntry>().apply {
|
2025-05-17 11:39:53 -05:00
|
|
|
fun addDevice(entry: DeviceListEntry) {
|
|
|
|
|
this[entry.fullAddress] = entry
|
|
|
|
|
}
|
2023-11-17 08:46:54 -03:00
|
|
|
|
|
|
|
|
// Include a placeholder for "None"
|
2025-06-29 14:18:14 +00:00
|
|
|
addDevice(
|
|
|
|
|
DeviceListEntry(
|
|
|
|
|
context.getString(R.string.none),
|
|
|
|
|
NO_DEVICE_SELECTED,
|
|
|
|
|
true
|
|
|
|
|
)
|
|
|
|
|
)
|
2023-11-17 08:46:54 -03:00
|
|
|
|
2024-10-02 19:58:11 -03:00
|
|
|
if (showMockInterface) {
|
|
|
|
|
addDevice(DeviceListEntry("Demo Mode", "m", true))
|
2023-11-17 08:46:54 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Include paired Bluetooth devices
|
2025-05-17 11:39:53 -05:00
|
|
|
ble.bondedDevices.map(::BLEDeviceListEntry).sortedBy { it.name }
|
|
|
|
|
.forEach(::addDevice)
|
2023-11-17 08:46:54 -03:00
|
|
|
|
|
|
|
|
// Include Network Service Discovery
|
|
|
|
|
tcp.forEach { service ->
|
2025-03-17 20:13:27 -03:00
|
|
|
val address = service.toAddressString()
|
2025-06-29 14:18:14 +00:00
|
|
|
val txtRecords = service.attributes // Map<String, ByteArray?>
|
|
|
|
|
val shortNameBytes = txtRecords["shortname"]
|
|
|
|
|
val idBytes = txtRecords["id"]
|
|
|
|
|
|
|
|
|
|
val shortName = shortNameBytes?.let { String(it, Charsets.UTF_8) }
|
|
|
|
|
?: context.getString(R.string.meshtastic)
|
|
|
|
|
val deviceId =
|
|
|
|
|
idBytes?.let { String(it, Charsets.UTF_8) }?.replace("!", "")
|
|
|
|
|
var displayName = shortName
|
|
|
|
|
if (deviceId != null) {
|
|
|
|
|
displayName += "_$deviceId"
|
|
|
|
|
}
|
|
|
|
|
addDevice(DeviceListEntry(displayName, "t$address", true))
|
2023-11-17 08:46:54 -03:00
|
|
|
}
|
|
|
|
|
|
2025-07-03 13:26:59 +02:00
|
|
|
// Include saved IP connections
|
2025-07-21 00:48:34 +10:00
|
|
|
recent.forEach { (address, name) ->
|
|
|
|
|
addDevice(DeviceListEntry(name, address, true))
|
2025-07-03 13:26:59 +02:00
|
|
|
}
|
|
|
|
|
|
2023-11-17 08:46:54 -03:00
|
|
|
usb.forEach { (_, d) ->
|
|
|
|
|
addDevice(USBDeviceListEntry(radioInterfaceService, usbManagerLazy.get(), d))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.launchIn(viewModelScope)
|
2023-03-27 15:27:26 -03:00
|
|
|
|
2024-06-03 10:17:20 -03:00
|
|
|
serviceRepository.statusMessage
|
|
|
|
|
.onEach { errorText.value = it }
|
|
|
|
|
.launchIn(viewModelScope)
|
|
|
|
|
|
2022-06-12 16:32:06 -03:00
|
|
|
debug("BTScanModel created")
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-02 17:23:11 -03:00
|
|
|
/**
|
|
|
|
|
* @param fullAddress Interface [prefix] + [address] (example: "x7C:9E:BD:F0:BE:BE")
|
|
|
|
|
*/
|
2022-06-12 16:32:06 -03:00
|
|
|
open class DeviceListEntry(val name: String, val fullAddress: String, val bonded: Boolean) {
|
|
|
|
|
val prefix get() = fullAddress[0]
|
|
|
|
|
val address get() = fullAddress.substring(1)
|
|
|
|
|
|
|
|
|
|
override fun toString(): String {
|
|
|
|
|
return "DeviceListEntry(name=${name.anonymize}, addr=${address.anonymize}, bonded=$bonded)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
val isBLE: Boolean get() = prefix == 'x'
|
|
|
|
|
val isUSB: Boolean get() = prefix == 's'
|
|
|
|
|
val isTCP: Boolean get() = prefix == 't'
|
2025-06-23 20:13:09 +00:00
|
|
|
|
|
|
|
|
val isMock: Boolean get() = prefix == 'm'
|
|
|
|
|
val isDisconnect: Boolean get() = prefix == 'n'
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
|
|
|
|
|
2023-04-01 08:03:32 -03:00
|
|
|
@SuppressLint("MissingPermission")
|
|
|
|
|
class BLEDeviceListEntry(device: BluetoothDevice) : DeviceListEntry(
|
2023-06-02 17:23:11 -03:00
|
|
|
device.name ?: "unnamed-${device.address}", // some devices might not have a name
|
2023-04-01 08:03:32 -03:00
|
|
|
"x${device.address}",
|
|
|
|
|
device.bondState == BluetoothDevice.BOND_BONDED
|
|
|
|
|
)
|
|
|
|
|
|
2023-10-24 12:09:18 -07:00
|
|
|
class USBDeviceListEntry(
|
|
|
|
|
radioInterfaceService: RadioInterfaceService,
|
|
|
|
|
usbManager: UsbManager,
|
|
|
|
|
val usb: UsbSerialDriver,
|
|
|
|
|
) : DeviceListEntry(
|
2022-06-12 16:32:06 -03:00
|
|
|
usb.device.deviceName,
|
2023-10-24 12:09:18 -07:00
|
|
|
radioInterfaceService.toInterfaceAddress(InterfaceId.SERIAL, usb.device.deviceName),
|
|
|
|
|
usbManager.hasPermission(usb.device),
|
2022-06-12 16:32:06 -03:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
override fun onCleared() {
|
|
|
|
|
super.onCleared()
|
|
|
|
|
debug("BTScanModel cleared")
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-13 17:20:26 -03:00
|
|
|
fun setErrorText(text: String) {
|
|
|
|
|
errorText.value = text
|
|
|
|
|
}
|
2022-06-12 16:32:06 -03:00
|
|
|
|
2023-11-17 08:46:54 -03:00
|
|
|
private var scanJob: Job? = null
|
2022-06-12 16:32:06 -03:00
|
|
|
|
2025-06-13 06:34:44 -05:00
|
|
|
val selectedAddressFlow: StateFlow<String?> = radioInterfaceService.currentDeviceAddressFlow
|
|
|
|
|
|
|
|
|
|
val selectedNotNullFlow: StateFlow<String> = selectedAddressFlow
|
|
|
|
|
.map { it ?: NO_DEVICE_SELECTED }
|
|
|
|
|
.stateIn(
|
|
|
|
|
viewModelScope,
|
|
|
|
|
SharingStarted.WhileSubscribed(SHARING_STARTED_TIMEOUT_MS),
|
|
|
|
|
selectedAddressFlow.value ?: NO_DEVICE_SELECTED
|
|
|
|
|
)
|
2022-06-12 16:32:06 -03:00
|
|
|
|
2024-08-03 07:53:59 -03:00
|
|
|
val scanResult = MutableLiveData<MutableMap<String, DeviceListEntry>>(mutableMapOf())
|
|
|
|
|
|
|
|
|
|
fun clearScanResults() {
|
|
|
|
|
stopScan()
|
|
|
|
|
scanResult.value = mutableMapOf()
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun stopScan() {
|
2023-11-17 08:46:54 -03:00
|
|
|
if (scanJob != null) {
|
2022-06-12 16:32:06 -03:00
|
|
|
debug("stopping scan")
|
|
|
|
|
try {
|
2023-11-17 08:46:54 -03:00
|
|
|
scanJob?.cancel()
|
2022-06-12 16:32:06 -03:00
|
|
|
} catch (ex: Throwable) {
|
|
|
|
|
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
|
|
|
|
|
} finally {
|
2023-11-17 08:46:54 -03:00
|
|
|
scanJob = null
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
2024-08-03 07:53:59 -03:00
|
|
|
}
|
|
|
|
|
_spinner.value = false
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@SuppressLint("MissingPermission")
|
2024-08-03 07:53:59 -03:00
|
|
|
fun startScan() {
|
2023-11-17 08:46:54 -03:00
|
|
|
debug("starting classic scan")
|
|
|
|
|
|
2024-08-03 07:53:59 -03:00
|
|
|
_spinner.value = true
|
2023-11-17 08:46:54 -03:00
|
|
|
scanJob = bluetoothRepository.scan()
|
|
|
|
|
.onEach { result ->
|
2025-05-17 11:39:53 -05:00
|
|
|
val fullAddress = radioInterfaceService.toInterfaceAddress(
|
|
|
|
|
InterfaceId.BLUETOOTH,
|
|
|
|
|
result.device.address
|
|
|
|
|
)
|
|
|
|
|
// prevent log spam because we'll get lots of redundant scan results
|
|
|
|
|
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
|
|
|
|
|
val oldDevs = scanResult.value!!
|
|
|
|
|
val oldEntry = oldDevs[fullAddress]
|
|
|
|
|
// Don't spam the GUI with endless updates for non changing nodes
|
|
|
|
|
if (oldEntry == null || oldEntry.bonded != isBonded) {
|
|
|
|
|
val entry = DeviceListEntry(result.device.name, fullAddress, isBonded)
|
|
|
|
|
oldDevs[entry.fullAddress] = entry
|
|
|
|
|
scanResult.value = oldDevs
|
|
|
|
|
}
|
|
|
|
|
}.catch { ex ->
|
|
|
|
|
serviceRepository.setErrorMessage("Unexpected Bluetooth scan failure: ${ex.message}")
|
|
|
|
|
}.launchIn(viewModelScope)
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
|
|
|
|
|
2023-11-17 08:46:54 -03:00
|
|
|
private fun changeDeviceAddress(address: String) {
|
|
|
|
|
try {
|
|
|
|
|
serviceRepository.meshService?.let { service ->
|
|
|
|
|
MeshService.changeDeviceAddress(context, service, address)
|
|
|
|
|
}
|
|
|
|
|
devices.value = devices.value // Force a GUI update
|
|
|
|
|
} catch (ex: RemoteException) {
|
|
|
|
|
errormsg("changeDeviceSelection failed, probably it is shutting down", ex)
|
|
|
|
|
// ignore the failure and the GUI won't be updating anyways
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-03 18:29:41 -03:00
|
|
|
|
2022-06-12 16:32:06 -03:00
|
|
|
@SuppressLint("MissingPermission")
|
2023-11-17 08:46:54 -03:00
|
|
|
private fun requestBonding(it: DeviceListEntry) {
|
|
|
|
|
val device = bluetoothRepository.getRemoteDevice(it.address) ?: return
|
|
|
|
|
info("Starting bonding for ${device.anonymize}")
|
|
|
|
|
|
|
|
|
|
bluetoothRepository.createBond(device)
|
|
|
|
|
.onEach { state ->
|
|
|
|
|
debug("Received bond state changed $state")
|
|
|
|
|
if (state != BluetoothDevice.BOND_BONDING) {
|
|
|
|
|
debug("Bonding completed, state=$state")
|
|
|
|
|
if (state == BluetoothDevice.BOND_BONDED) {
|
|
|
|
|
setErrorText(context.getString(R.string.pairing_completed))
|
|
|
|
|
changeDeviceAddress(it.fullAddress)
|
|
|
|
|
} else {
|
|
|
|
|
setErrorText(context.getString(R.string.pairing_failed_try_again))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}.catch { ex ->
|
|
|
|
|
// We ignore missing BT adapters, because it lets us run on the emulator
|
|
|
|
|
warn("Failed creating Bluetooth bond: ${ex.message}")
|
|
|
|
|
}.launchIn(viewModelScope)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun requestPermission(it: USBDeviceListEntry) {
|
|
|
|
|
usbRepository.requestPermission(it.usb.device)
|
|
|
|
|
.onEach { granted ->
|
|
|
|
|
if (granted) {
|
|
|
|
|
info("User approved USB access")
|
|
|
|
|
changeDeviceAddress(it.fullAddress)
|
|
|
|
|
} else {
|
|
|
|
|
errormsg("USB permission denied for device ${it.address}")
|
|
|
|
|
}
|
|
|
|
|
}.launchIn(viewModelScope)
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 00:48:34 +10:00
|
|
|
private fun getRecentAddresses(): List<Pair<String, String>> {
|
2025-07-03 13:26:59 +02:00
|
|
|
val jsonAddresses = preferences.getString("recent-ip-addresses", "[]") ?: "[]"
|
2025-07-21 00:48:34 +10:00
|
|
|
val jsonArray = JSONArray(jsonAddresses)
|
|
|
|
|
var needsMigration = false
|
2025-07-20 13:36:47 -05:00
|
|
|
|
|
|
|
|
val listAddresses = (0 until jsonArray.length()).mapNotNull { i ->
|
|
|
|
|
when (val item = jsonArray.get(i)) {
|
|
|
|
|
is JSONObject -> {
|
|
|
|
|
// Modern format: JSONObject with address and name
|
|
|
|
|
item.getString("address") to item.getString("name")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
is String -> {
|
|
|
|
|
// Old format: just the address string
|
|
|
|
|
needsMigration = true
|
|
|
|
|
item to context.getString(R.string.meshtastic) // [3]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else -> {
|
|
|
|
|
// Unknown format, log or handle as an error if necessary
|
|
|
|
|
warn("Unknown item type in recent IP addresses: $item")
|
|
|
|
|
null
|
|
|
|
|
}
|
2025-07-21 00:48:34 +10:00
|
|
|
}
|
|
|
|
|
}
|
2025-07-20 13:36:47 -05:00
|
|
|
|
|
|
|
|
// If migration was needed for any item, rewrite the entire list in the new format
|
2025-07-21 00:48:34 +10:00
|
|
|
if (needsMigration) {
|
|
|
|
|
setRecentAddresses(listAddresses)
|
2025-07-03 13:26:59 +02:00
|
|
|
}
|
|
|
|
|
return listAddresses
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 00:48:34 +10:00
|
|
|
private fun setRecentAddresses(addresses: List<Pair<String, String>>) {
|
|
|
|
|
val jsonArray = JSONArray()
|
|
|
|
|
addresses.forEach { (address, name) ->
|
2025-07-20 13:36:47 -05:00
|
|
|
val obj = JSONObject()
|
2025-07-21 00:48:34 +10:00
|
|
|
obj.put("address", address)
|
|
|
|
|
obj.put("name", name)
|
|
|
|
|
jsonArray.put(obj)
|
|
|
|
|
}
|
2025-07-05 22:32:03 +00:00
|
|
|
preferences.edit {
|
2025-07-21 00:48:34 +10:00
|
|
|
putString("recent-ip-addresses", jsonArray.toString())
|
2025-07-05 22:32:03 +00:00
|
|
|
}
|
2025-07-03 13:26:59 +02:00
|
|
|
recentIpAddresses.value = addresses
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-21 00:48:34 +10:00
|
|
|
// Remove 'name' parameter from addRecentAddress and related logic
|
|
|
|
|
fun addRecentAddress(address: String, overrideName: String? = null) {
|
2025-07-03 13:26:59 +02:00
|
|
|
if (!address.startsWith("t")) return
|
|
|
|
|
val existingItems = getRecentAddresses()
|
2025-07-21 00:48:34 +10:00
|
|
|
val updatedList = mutableListOf<Pair<String, String>>()
|
|
|
|
|
val displayName = overrideName ?: context.getString(R.string.meshtastic)
|
|
|
|
|
updatedList.add(address to displayName)
|
|
|
|
|
updatedList.addAll(existingItems.filter { it.first != address }.take(2))
|
|
|
|
|
setRecentAddresses(updatedList)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fun removeRecentAddress(address: String) {
|
|
|
|
|
val existingItems = getRecentAddresses()
|
|
|
|
|
val updatedList = existingItems.filter { it.first != address }
|
2025-07-03 13:26:59 +02:00
|
|
|
setRecentAddresses(updatedList)
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-17 08:46:54 -03:00
|
|
|
// Called by the GUI when a new device has been selected by the user
|
|
|
|
|
// @returns true if we were able to change to that item
|
|
|
|
|
fun onSelected(it: DeviceListEntry): Boolean {
|
|
|
|
|
// If the device is paired, let user select it, otherwise start the pairing flow
|
|
|
|
|
if (it.bonded) {
|
2025-07-21 00:48:34 +10:00
|
|
|
addRecentAddress(it.fullAddress, connectedNodeLongName)
|
2023-11-17 08:46:54 -03:00
|
|
|
changeDeviceAddress(it.fullAddress)
|
|
|
|
|
return true
|
2022-06-12 16:32:06 -03:00
|
|
|
} else {
|
2023-11-17 08:46:54 -03:00
|
|
|
// Handle requesting USB or bluetooth permissions for the device
|
|
|
|
|
debug("Requesting permissions for the device")
|
|
|
|
|
|
|
|
|
|
if (it.isBLE) {
|
|
|
|
|
requestBonding(it)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (it.isUSB) {
|
|
|
|
|
requestPermission(it as USBDeviceListEntry)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
2022-06-12 16:32:06 -03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 16:43:04 +00:00
|
|
|
private val _spinner = MutableStateFlow(false)
|
|
|
|
|
val spinner: StateFlow<Boolean> get() = _spinner.asStateFlow()
|
2025-07-21 00:48:34 +10:00
|
|
|
|
|
|
|
|
// Add a new property to hold the connected node's long name
|
|
|
|
|
var connectedNodeLongName: String? = null
|
2023-04-03 18:29:41 -03:00
|
|
|
}
|
2025-05-30 13:17:09 -05:00
|
|
|
|
|
|
|
|
const val NO_DEVICE_SELECTED = "n"
|
2025-06-13 06:34:44 -05:00
|
|
|
private const val SHARING_STARTED_TIMEOUT_MS = 5000L
|