mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
chore: review-cleanup fleet (audit + fix + hardening) (#5158)
This commit is contained in:
parent
872c566ef1
commit
17e69c6d4c
68 changed files with 784 additions and 459 deletions
|
|
@ -18,14 +18,15 @@ package org.meshtastic.feature.connections.domain.usecase
|
|||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
import org.meshtastic.core.common.util.safeCatchingAll
|
||||
import org.meshtastic.core.datastore.RecentAddressesDataSource
|
||||
import org.meshtastic.core.network.repository.NetworkRepository
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.demo_mode
|
||||
import org.meshtastic.core.resources.getStringSuspend
|
||||
import org.meshtastic.core.resources.meshtastic
|
||||
import org.meshtastic.feature.connections.model.DeviceListEntry
|
||||
import org.meshtastic.feature.connections.model.DiscoveredDevices
|
||||
|
|
@ -49,7 +50,7 @@ class CommonGetDiscoveredDevicesUseCase(
|
|||
tcpServices,
|
||||
recentList,
|
||||
->
|
||||
val defaultName = runCatching { getString(Res.string.meshtastic) }.getOrDefault("Meshtastic")
|
||||
val defaultName = safeCatchingAll { getStringSuspend(Res.string.meshtastic) }.getOrDefault("Meshtastic")
|
||||
processTcpServices(tcpServices, recentList, defaultName)
|
||||
}
|
||||
|
||||
|
|
@ -71,7 +72,7 @@ class CommonGetDiscoveredDevicesUseCase(
|
|||
usbList +
|
||||
if (showMock) {
|
||||
val demoModeLabel =
|
||||
runCatching { getString(Res.string.demo_mode) }.getOrDefault("Demo Mode")
|
||||
safeCatchingAll { getStringSuspend(Res.string.demo_mode) }.getOrDefault("Demo Mode")
|
||||
listOf(DeviceListEntry.Mock(demoModeLabel))
|
||||
} else {
|
||||
emptyList()
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.runtime.LaunchedEffect
|
|||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
|
@ -75,15 +76,11 @@ fun DeviceListItem(
|
|||
) {
|
||||
// Throttle the RSSI updates to match the connected device polling rate
|
||||
var displayedRssi by remember { mutableIntStateOf(rssi ?: 0) }
|
||||
LaunchedEffect(rssi) {
|
||||
if (displayedRssi == 0) {
|
||||
displayedRssi = rssi ?: 0
|
||||
}
|
||||
}
|
||||
val currentRssi by rememberUpdatedState(rssi)
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(RSSI_UPDATE_RATE_MS)
|
||||
displayedRssi = rssi ?: 0
|
||||
displayedRssi = currentRssi ?: 0
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import co.touchlab.kermit.Logger
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
|
@ -48,7 +47,7 @@ class BleOtaTransport(
|
|||
private val scanner: BleScanner,
|
||||
connectionFactory: BleConnectionFactory,
|
||||
private val address: String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
dispatcher: CoroutineDispatcher,
|
||||
) : UnifiedOtaProtocol {
|
||||
|
||||
private val transportScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import org.meshtastic.core.common.util.CommonUri
|
|||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
|
|
@ -67,7 +68,7 @@ private const val GATT_RELEASE_DELAY_MS = 1000L
|
|||
*
|
||||
* All platform I/O (file reading, content-resolver imports) is delegated to [FirmwareFileHandler].
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@Suppress("TooManyFunctions", "LongParameterList")
|
||||
@Single
|
||||
class Esp32OtaUpdateHandler(
|
||||
private val firmwareRetriever: FirmwareRetriever,
|
||||
|
|
@ -76,6 +77,7 @@ class Esp32OtaUpdateHandler(
|
|||
private val nodeRepository: NodeRepository,
|
||||
private val bleScanner: BleScanner,
|
||||
private val bleConnectionFactory: BleConnectionFactory,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : FirmwareUpdateHandler {
|
||||
|
||||
/** Entry point for FirmwareUpdateHandler interface. Routes to BLE (MAC with colons) or WiFi (IP without). */
|
||||
|
|
@ -102,7 +104,7 @@ class Esp32OtaUpdateHandler(
|
|||
hardware = hardware,
|
||||
updateState = updateState,
|
||||
firmwareUri = firmwareUri,
|
||||
transportFactory = { BleOtaTransport(bleScanner, bleConnectionFactory, address) },
|
||||
transportFactory = { BleOtaTransport(bleScanner, bleConnectionFactory, address, dispatchers.default) },
|
||||
rebootMode = 1,
|
||||
connectionAttempts = 5,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ class WifiOtaTransport(private val deviceIpAddress: String, private val port: In
|
|||
|
||||
override suspend fun close() {
|
||||
withContext(ioDispatcher) {
|
||||
runCatching {
|
||||
safeCatching {
|
||||
socket?.close()
|
||||
selectorManager?.close()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import org.meshtastic.core.common.util.CommonUri
|
|||
import org.meshtastic.core.common.util.NumberFormatter
|
||||
import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.resources.Res
|
||||
|
|
@ -70,6 +71,7 @@ class SecureDfuHandler(
|
|||
private val radioController: RadioController,
|
||||
private val bleScanner: BleScanner,
|
||||
private val bleConnectionFactory: BleConnectionFactory,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : FirmwareUpdateHandler {
|
||||
|
||||
@Suppress("LongMethod")
|
||||
|
|
@ -108,7 +110,7 @@ class SecureDfuHandler(
|
|||
var transport: SecureDfuTransport? = null
|
||||
var completed = false
|
||||
try {
|
||||
transport = SecureDfuTransport(bleScanner, bleConnectionFactory, target)
|
||||
transport = SecureDfuTransport(bleScanner, bleConnectionFactory, target, dispatchers.default)
|
||||
|
||||
transport.triggerButtonlessDfu().onFailure { e ->
|
||||
Logger.w(e) { "DFU: Buttonless trigger failed ($e) — device may already be in DFU mode" }
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ import kotlinx.coroutines.CancellationException
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.TimeoutCancellationException
|
||||
import kotlinx.coroutines.cancel
|
||||
|
|
@ -67,7 +66,7 @@ class SecureDfuTransport(
|
|||
private val scanner: BleScanner,
|
||||
connectionFactory: BleConnectionFactory,
|
||||
private val address: String,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
private val transportScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
private val bleConnection = connectionFactory.create(transportScope, "Secure DFU")
|
||||
|
|
@ -252,7 +251,7 @@ class SecureDfuTransport(
|
|||
* accept a fresh DFU session.
|
||||
*/
|
||||
suspend fun abort() {
|
||||
runCatching {
|
||||
safeCatching {
|
||||
bleConnection.profile(SecureDfuUuids.SERVICE) { service ->
|
||||
val controlChar = service.characteristic(SecureDfuUuids.CONTROL_POINT)
|
||||
service.write(controlChar, byteArrayOf(DfuOpcode.ABORT), BleWriteType.WITH_RESPONSE)
|
||||
|
|
@ -264,7 +263,7 @@ class SecureDfuTransport(
|
|||
|
||||
/** Disconnect from the DFU target and cancel the transport coroutine scope. */
|
||||
suspend fun close() {
|
||||
runCatching { bleConnection.disconnect() }.onFailure { Logger.w(it) { "DFU: Error during disconnect" } }
|
||||
safeCatching { bleConnection.disconnect() }.onFailure { Logger.w(it) { "DFU: Error during disconnect" } }
|
||||
transportScope.cancel()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ import dev.mokkery.MockMode
|
|||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
|
|
@ -59,6 +61,12 @@ class DefaultFirmwareUpdateManagerTest {
|
|||
private val bleScanner: BleScanner = mock(MockMode.autofill)
|
||||
private val bleConnectionFactory: BleConnectionFactory = mock(MockMode.autofill)
|
||||
private val firmwareRetriever = FirmwareRetriever(fileHandler)
|
||||
private val dispatchers =
|
||||
CoroutineDispatchers(
|
||||
io = Dispatchers.Unconfined,
|
||||
main = Dispatchers.Unconfined,
|
||||
default = Dispatchers.Unconfined,
|
||||
)
|
||||
|
||||
private val secureDfuHandler =
|
||||
SecureDfuHandler(
|
||||
|
|
@ -67,6 +75,7 @@ class DefaultFirmwareUpdateManagerTest {
|
|||
radioController = radioController,
|
||||
bleScanner = bleScanner,
|
||||
bleConnectionFactory = bleConnectionFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private val usbUpdateHandler =
|
||||
|
|
@ -84,6 +93,7 @@ class DefaultFirmwareUpdateManagerTest {
|
|||
nodeRepository = nodeRepository,
|
||||
bleScanner = bleScanner,
|
||||
bleConnectionFactory = bleConnectionFactory,
|
||||
dispatchers = dispatchers,
|
||||
)
|
||||
|
||||
private fun createManager(address: String?): DefaultFirmwareUpdateManager {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
plugins {
|
||||
alias(libs.plugins.meshtastic.kmp.feature)
|
||||
alias(libs.plugins.meshtastic.kotlinx.serialization)
|
||||
id("meshtastic.kmp.jvm.android")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
|||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
|
||||
private const val SDK_INT_ANDROID_16 = 37
|
||||
private val SDK_INT_ANDROID_16 = Build.VERSION_CODES.BAKLAVA
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-2026 Meshtastic LLC
|
||||
*
|
||||
* 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/>.
|
||||
*/
|
||||
package org.meshtastic.feature.settings.navigation
|
||||
|
||||
import org.meshtastic.core.navigation.SettingsRoute
|
||||
|
||||
actual fun getAboutLibrariesJson(): String =
|
||||
SettingsRoute::class.java.getResource("/aboutlibraries.json")?.readText() ?: ""
|
||||
|
|
@ -26,14 +26,12 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import org.koin.core.annotation.Single
|
||||
import org.meshtastic.core.common.util.nowMillis
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.util.onlineTimeThreshold
|
||||
import org.meshtastic.core.repository.AppWidgetUpdater
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.ServiceRepository
|
||||
import org.meshtastic.proto.LocalStats
|
||||
|
|
@ -78,12 +76,10 @@ data class LocalStatsWidgetUiState(
|
|||
val updateTimeMillis: Long = 0,
|
||||
)
|
||||
|
||||
private const val WIDGET_SUBSCRIPTION_TIMEOUT_MS = 5_000L
|
||||
|
||||
@Single
|
||||
class LocalStatsWidgetStateProvider(
|
||||
nodeRepository: NodeRepository,
|
||||
serviceRepository: ServiceRepository,
|
||||
appWidgetUpdater: AppWidgetUpdater,
|
||||
) {
|
||||
class LocalStatsWidgetStateProvider(nodeRepository: NodeRepository, serviceRepository: ServiceRepository) {
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class)
|
||||
|
|
@ -105,8 +101,11 @@ class LocalStatsWidgetStateProvider(
|
|||
mapToUiState(input.connectionState, input.totalNodes, input.onlineNodes, input.stats, input.localNode)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
.onEach { appWidgetUpdater.updateAll() }
|
||||
.stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = LocalStatsWidgetUiState())
|
||||
.stateIn(
|
||||
scope = scope,
|
||||
started = SharingStarted.WhileSubscribed(WIDGET_SUBSCRIPTION_TIMEOUT_MS),
|
||||
initialValue = LocalStatsWidgetUiState(),
|
||||
)
|
||||
|
||||
private data class StateInput(
|
||||
val connectionState: ConnectionState,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ kotlin {
|
|||
commonMain.dependencies {
|
||||
implementation(projects.core.ble)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.di)
|
||||
implementation(projects.core.navigation)
|
||||
implementation(projects.core.resources)
|
||||
implementation(projects.core.ui)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ import kotlinx.coroutines.launch
|
|||
import org.koin.core.annotation.KoinViewModel
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.feature.wifiprovision.domain.NymeaWifiService
|
||||
import org.meshtastic.feature.wifiprovision.model.ProvisionResult
|
||||
import org.meshtastic.feature.wifiprovision.model.WifiNetwork
|
||||
|
|
@ -106,6 +107,7 @@ sealed interface WifiProvisionError {
|
|||
class WifiProvisionViewModel(
|
||||
private val bleScanner: BleScanner,
|
||||
private val bleConnectionFactory: BleConnectionFactory,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(WifiProvisionUiState())
|
||||
|
|
@ -127,7 +129,7 @@ class WifiProvisionViewModel(
|
|||
_uiState.update { it.copy(phase = WifiProvisionUiState.Phase.ConnectingBle, error = null) }
|
||||
|
||||
viewModelScope.launch {
|
||||
val nymeaService = NymeaWifiService(bleScanner, bleConnectionFactory)
|
||||
val nymeaService = NymeaWifiService(bleScanner, bleConnectionFactory, dispatchers.default)
|
||||
service = nymeaService
|
||||
|
||||
nymeaService
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import co.touchlab.kermit.Logger
|
|||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
|
@ -67,7 +66,7 @@ import org.meshtastic.feature.wifiprovision.model.WifiNetwork
|
|||
class NymeaWifiService(
|
||||
private val scanner: BleScanner,
|
||||
connectionFactory: BleConnectionFactory,
|
||||
dispatcher: CoroutineDispatcher = Dispatchers.Default,
|
||||
dispatcher: CoroutineDispatcher,
|
||||
) {
|
||||
|
||||
private val serviceScope = CoroutineScope(SupervisorJob() + dispatcher)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import kotlinx.coroutines.test.advanceUntilIdle
|
|||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.testing.FakeBleConnection
|
||||
import org.meshtastic.core.testing.FakeBleConnectionFactory
|
||||
import org.meshtastic.core.testing.FakeBleDevice
|
||||
|
|
@ -62,7 +63,15 @@ class WifiProvisionViewModelTest {
|
|||
scanner = FakeBleScanner()
|
||||
connection = FakeBleConnection()
|
||||
viewModel =
|
||||
WifiProvisionViewModel(bleScanner = scanner, bleConnectionFactory = FakeBleConnectionFactory(connection))
|
||||
WifiProvisionViewModel(
|
||||
bleScanner = scanner,
|
||||
bleConnectionFactory = FakeBleConnectionFactory(connection),
|
||||
dispatchers = CoroutineDispatchers(
|
||||
io = testDispatcher,
|
||||
main = testDispatcher,
|
||||
default = testDispatcher,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue