chore: review-cleanup fleet (audit + fix + hardening) (#5158)

This commit is contained in:
James Rich 2026-04-16 19:02:59 -05:00 committed by GitHub
parent 872c566ef1
commit 17e69c6d4c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 784 additions and 459 deletions

View file

@ -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()

View file

@ -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
}
}

View file

@ -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)

View file

@ -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,
)

View file

@ -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()
}

View file

@ -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" }

View file

@ -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()
}

View file

@ -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 {

View file

@ -18,6 +18,7 @@
plugins {
alias(libs.plugins.meshtastic.kmp.feature)
alias(libs.plugins.meshtastic.kotlinx.serialization)
id("meshtastic.kmp.jvm.android")
}
kotlin {

View file

@ -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

View file

@ -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() ?: ""

View file

@ -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,

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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