chore: KMP audit — commonize code, centralize utilities, eliminate dead abstractions (#5133)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
James Rich 2026-04-14 21:17:50 -05:00 committed by GitHub
parent 50ade01e55
commit 72b981f73b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
132 changed files with 2186 additions and 916 deletions

View file

@ -30,15 +30,16 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.eygraber.uri.toKmpUri
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.common.util.toMeshtasticUri
import org.meshtastic.core.navigation.Route
import org.meshtastic.core.navigation.SettingsRoute
import org.meshtastic.core.navigation.WifiProvisionRoute
@ -89,14 +90,14 @@ fun SettingsScreen(
val state by viewModel.radioConfigState.collectAsStateWithLifecycle()
var deviceProfile by remember { mutableStateOf<DeviceProfile?>(null) }
var showEditDeviceProfileDialog by remember { mutableStateOf(false) }
var showEditDeviceProfileDialog by rememberSaveable { mutableStateOf(false) }
val importConfigLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
showEditDeviceProfileDialog = true
it.data?.data?.let { uri ->
viewModel.importProfile(uri.toMeshtasticUri()) { profile -> deviceProfile = profile }
viewModel.importProfile(uri.toKmpUri()) { profile -> deviceProfile = profile }
}
}
}
@ -104,7 +105,7 @@ fun SettingsScreen(
val exportConfigLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> viewModel.exportProfile(uri.toMeshtasticUri(), deviceProfile!!) }
it.data?.data?.let { uri -> viewModel.exportProfile(uri.toKmpUri(), deviceProfile!!) }
}
}
@ -143,12 +144,12 @@ fun SettingsScreen(
)
}
var showLanguagePickerDialog by remember { mutableStateOf(false) }
var showLanguagePickerDialog by rememberSaveable { mutableStateOf(false) }
if (showLanguagePickerDialog) {
LanguagePickerDialog { showLanguagePickerDialog = false }
}
var showThemePickerDialog by remember { mutableStateOf(false) }
var showThemePickerDialog by rememberSaveable { mutableStateOf(false) }
if (showThemePickerDialog) {
ThemePickerDialog(
onClickTheme = { settingsViewModel.setTheme(it) },
@ -249,7 +250,7 @@ fun SettingsScreen(
cacheLimit = settingsViewModel.dbCacheLimit.collectAsStateWithLifecycle().value,
onSetCacheLimit = { settingsViewModel.setDbCacheLimit(it) },
nodeShortName = ourNode?.user?.short_name ?: "",
onExportData = { settingsViewModel.saveDataCsv(it.toMeshtasticUri()) },
onExportData = { settingsViewModel.saveDataCsv(it.toKmpUri()) },
)
AppInfoSection(

View file

@ -30,9 +30,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.eygraber.uri.toKmpUri
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.common.util.toMeshtasticUri
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.export_keys
import org.meshtastic.core.resources.export_keys_confirmation
@ -54,7 +54,7 @@ actual fun ExportSecurityConfigButton(
val exportConfigLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri.toMeshtasticUri(), securityConfig) }
it.data?.data?.let { uri -> viewModel.exportSecurityConfig(uri.toKmpUri(), securityConfig) }
}
}

View file

@ -28,7 +28,7 @@ import okio.BufferedSink
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
@ -187,7 +187,7 @@ class SettingsViewModel(
* @param uri The destination URI for the CSV file.
* @param filterPortnum If provided, only packets with this port number will be exported.
*/
fun saveDataCsv(uri: MeshtasticUri, filterPortnum: Int? = null) {
fun saveDataCsv(uri: CommonUri, filterPortnum: Int? = null) {
safeLaunch(tag = "saveDataCsv") {
fileService.write(uri) { writer -> performDataExport(writer, filterPortnum) }
}

View file

@ -35,7 +35,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -158,7 +158,7 @@ fun DebugSearchState(
onExportLogs: (() -> Unit)? = null,
) {
val colorScheme = MaterialTheme.colorScheme
var customFilterText by remember { mutableStateOf("") }
var customFilterText by rememberSaveable { mutableStateOf("") }
Column(modifier = modifier.background(color = colorScheme.background.copy(alpha = 1.0f)).padding(8.dp)) {
Row(

View file

@ -61,15 +61,6 @@ import org.meshtastic.proto.Telemetry
import org.meshtastic.proto.User
import org.meshtastic.proto.Waypoint
data class SearchMatch(val logIndex: Int, val start: Int, val end: Int, val field: String)
data class SearchState(
val searchText: String = "",
val currentMatchIndex: Int = -1,
val allMatches: List<SearchMatch> = emptyList(),
val hasMatches: Boolean = false,
)
enum class FilterMode {
AND,
OR,
@ -387,17 +378,15 @@ class DebugViewModel(
val nodeIdStr = nodeId.toUInt().toString()
// Only match if whitespace before and after
val regex = Regex("""(?<=\s|^)${Regex.escape(nodeIdStr)}(?=\s|$)""")
regex.find(this)?.let { _ ->
regex.findAll(this).toList().asReversed().forEach {
val idx = it.range.last + 1
insert(idx, " (${nodeId.toHex(8)})")
}
return true
if (!regex.containsMatchIn(this)) return false
regex.findAll(this).toList().asReversed().forEach {
val idx = it.range.last + 1
insert(idx, " (${nodeId.toHex(8)})")
}
return false
return true
}
private fun Int.toHex(length: Int): String = "!" + this.toUInt().toString(16).padStart(length, '0')
private fun Int.toHex(length: Int): String = "!${this.toUInt().toString(16).padStart(length, '0')}"
fun requestDeleteAllLogs() {
alertManager.showAlert(

View file

@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -80,7 +79,7 @@ fun getRadioConfigViewModel(backStack: NavBackStack<NavKey>): RadioConfigViewMod
.lastOrNull { it is SettingsRoute.SettingsGraph }
?.let { (it as SettingsRoute.SettingsGraph).destNum }
}
SideEffect { viewModel.initDestNum(destNum) }
LaunchedEffect(destNum) { viewModel.initDestNum(destNum) }
return viewModel
}

View file

@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.update
import org.jetbrains.compose.resources.StringResource
import org.koin.core.annotation.InjectedParam
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.domain.usecase.settings.AdminActionsUseCase
import org.meshtastic.core.domain.usecase.settings.ExportProfileUseCase
import org.meshtastic.core.domain.usecase.settings.ExportSecurityConfigUseCase
@ -384,7 +384,7 @@ open class RadioConfigViewModel(
safeLaunch(tag = "removeFixedPosition") { radioConfigUseCase.removeFixedPosition(destNum) }
}
fun importProfile(uri: MeshtasticUri, onResult: (DeviceProfile) -> Unit) {
fun importProfile(uri: CommonUri, onResult: (DeviceProfile) -> Unit) {
safeLaunch(tag = "importProfile") {
var profile: DeviceProfile? = null
fileService.read(uri) { source ->
@ -394,7 +394,7 @@ open class RadioConfigViewModel(
}
}
fun exportProfile(uri: MeshtasticUri, profile: DeviceProfile) {
fun exportProfile(uri: CommonUri, profile: DeviceProfile) {
safeLaunch(tag = "exportProfile") {
fileService.write(uri) { sink ->
exportProfileUseCase(sink, profile).onSuccess { /* Success */ }.onFailure { throw it }
@ -402,7 +402,7 @@ open class RadioConfigViewModel(
}
}
fun exportSecurityConfig(uri: MeshtasticUri, securityConfig: Config.SecurityConfig) {
fun exportSecurityConfig(uri: CommonUri, securityConfig: Config.SecurityConfig) {
safeLaunch(tag = "exportSecurityConfig") {
fileService.write(uri) { sink ->
exportSecurityConfigUseCase(sink, securityConfig).onSuccess { /* Success */ }.onFailure { throw it }

View file

@ -113,9 +113,9 @@ private fun ChannelConfigScreen(
onPositiveClicked: (List<ChannelSettings>) -> Unit,
) {
val primarySettings = settingsList.getOrNull(0) ?: return
val modemPresetName by remember(loraConfig) { mutableStateOf(Channel(loraConfig = loraConfig).name) }
val primaryChannel by remember(loraConfig) { mutableStateOf(Channel(primarySettings, loraConfig)) }
val capabilities by remember(firmwareVersion) { mutableStateOf(Capabilities(firmwareVersion)) }
val modemPresetName = remember(loraConfig) { Channel(loraConfig = loraConfig).name }
val primaryChannel = remember(loraConfig) { Channel(primarySettings, loraConfig) }
val capabilities = remember(firmwareVersion) { Capabilities(firmwareVersion) }
val focusManager = LocalFocusManager.current
val settingsListInput =
@ -141,7 +141,7 @@ private fun ChannelConfigScreen(
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
EditChannelDialog(
channelSettings = with(settingsListInput) { if (size > index) get(index) else ChannelSettings() },
channelSettings = settingsListInput.getOrNull(index) ?: ChannelSettings(),
modemPresetName = modemPresetName,
onAddClick = {
if (settingsListInput.size > index) {

View file

@ -124,7 +124,7 @@ fun ChannelScreen(
val modemPresetName by
remember(channels) { mutableStateOf(Channel(loraConfig = channels.lora_config ?: Config.LoRaConfig()).name) }
var showResetDialog by remember { mutableStateOf(false) }
var showResetDialog by rememberSaveable { mutableStateOf(false) }
var shouldAddChannelsState by remember { mutableStateOf(true) }
@ -211,7 +211,7 @@ fun ChannelScreen(
requestChannelSet?.let { ScannedQrCodeDialog(it, onDismiss = { viewModel.clearRequestChannelUrl() }) }
var showShareDialog by remember { mutableStateOf(false) }
var showShareDialog by rememberSaveable { mutableStateOf(false) }
if (showShareDialog) {
ChannelShareDialog(

View file

@ -71,7 +71,7 @@ fun LoRaConfigScreen(viewModel: RadioConfigViewModel, onBack: () -> Unit) {
val primarySettings = state.channelList.getOrNull(0) ?: return
val formState = rememberConfigState(initialValue = loraConfig)
val primaryChannel by remember(formState.value) { mutableStateOf(Channel(primarySettings, formState.value)) }
val primaryChannel = remember(formState.value) { Channel(primarySettings, formState.value) }
val focusManager = LocalFocusManager.current
RadioConfigScreenList(