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

@ -20,7 +20,6 @@ package org.meshtastic.core.ui.util
import android.content.ActivityNotFoundException
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
@ -36,13 +35,14 @@ import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleEventEffect
import co.touchlab.kermit.Logger
import com.eygraber.uri.toAndroidUri
import com.eygraber.uri.toKmpUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.meshtastic.core.common.gpsDisabled
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.MeshtasticUri
import java.net.URLEncoder
@Composable
@ -107,16 +107,14 @@ actual fun rememberOpenUrl(): (url: String) -> Unit {
@Composable
@Suppress("Wrapping")
actual fun rememberSaveFileLauncher(
onUriReceived: (org.meshtastic.core.common.util.MeshtasticUri) -> Unit,
onUriReceived: (org.meshtastic.core.common.util.CommonUri) -> Unit,
): (defaultFilename: String, mimeType: String) -> Unit {
val launcher =
androidx.activity.compose.rememberLauncherForActivityResult(
androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult(),
) { result ->
if (result.resultCode == android.app.Activity.RESULT_OK) {
result.data?.data?.let { uri ->
onUriReceived(uri.toString().let { org.meshtastic.core.common.util.MeshtasticUri(it) })
}
result.data?.data?.let { uri -> onUriReceived(uri.toKmpUri()) }
}
}
@ -137,7 +135,7 @@ actual fun rememberSaveFileLauncher(
actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeType: String) -> Unit {
val launcher =
rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri ->
onUriReceived(uri?.let { CommonUri(it) })
onUriReceived(uri?.let { it.toKmpUri() })
}
return remember(launcher) { { mimeType -> launcher.launch(mimeType) } }
}
@ -151,7 +149,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
withContext(Dispatchers.IO) {
@Suppress("TooGenericExceptionCaught")
try {
val androidUri = Uri.parse(uri.toString())
val androidUri = uri.toAndroidUri()
context.contentResolver.openInputStream(androidUri)?.use { stream ->
stream.bufferedReader().use { reader ->
val buffer = CharArray(maxChars)

View file

@ -62,12 +62,13 @@ fun <T : Enum<T>> DropDownPreference(
enumEntriesOf(selectedItem).filter { it.name != "UNRECOGNIZED" && !it.isDeprecatedEnumEntry() }
}
val items = enumConstants.map {
val label = itemLabel?.invoke(it) ?: it.name
val icon = itemIcon?.invoke(it)
val color = itemColor?.invoke(it)
DropDownItem(it, label, icon, color)
}
val items =
enumConstants.map {
val label = itemLabel?.invoke(it) ?: it.name
val icon = itemIcon?.invoke(it)
val color = itemColor?.invoke(it)
DropDownItem(it, label, icon, color)
}
DropDownPreference(
title = title,

View file

@ -23,7 +23,7 @@ import androidx.compose.material3.IconToggleButton
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.text.input.ImeAction
@ -49,7 +49,7 @@ fun EditPasswordPreference(
onValueChanged: (String) -> Unit,
modifier: Modifier = Modifier,
) {
var isPasswordVisible by remember { mutableStateOf(false) }
var isPasswordVisible by rememberSaveable { mutableStateOf(false) }
EditTextPreference(
title = title,

View file

@ -26,6 +26,7 @@ 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
@ -90,10 +91,10 @@ fun MeshtasticImportFAB(
) {
sharedContact?.let { importDialog(it, onDismissSharedContact) }
var expanded by remember { mutableStateOf(false) }
var showUrlDialog by remember { mutableStateOf(false) }
var isNfcScanning by remember { mutableStateOf(false) }
var showNfcDisabledDialog by remember { mutableStateOf(false) }
var expanded by rememberSaveable { mutableStateOf(false) }
var showUrlDialog by rememberSaveable { mutableStateOf(false) }
var isNfcScanning by rememberSaveable { mutableStateOf(false) }
var showNfcDisabledDialog by rememberSaveable { mutableStateOf(false) }
val openNfcSettings = rememberOpenNfcSettings()
val barcodeScanner = LocalBarcodeScannerProvider.current { contents -> contents?.let { onImport(it) } }

View file

@ -41,7 +41,7 @@ import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.bad
import org.meshtastic.core.resources.fair
@ -154,7 +154,7 @@ fun Snr(snr: Float, modifier: Modifier = Modifier) {
Text(
modifier = modifier,
text = formatString("%s %.2fdB", stringResource(Res.string.snr), snr),
text = "${stringResource(Res.string.snr)} ${MetricFormatter.snr(snr, decimalPlaces = 2)}",
color = color,
style = MaterialTheme.typography.labelSmall,
)
@ -172,7 +172,7 @@ fun Rssi(rssi: Int, modifier: Modifier = Modifier) {
}
Text(
modifier = modifier,
text = formatString("%s %ddBm", stringResource(Res.string.rssi), rssi),
text = "${stringResource(Res.string.rssi)} ${MetricFormatter.rssi(rssi)}",
color = color,
style = MaterialTheme.typography.labelSmall,
)

View file

@ -37,7 +37,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.unknown
import org.meshtastic.core.ui.icon.BatteryEmpty
@ -49,7 +49,6 @@ import org.meshtastic.core.ui.theme.StatusColors.StatusGreen
import org.meshtastic.core.ui.theme.StatusColors.StatusOrange
import org.meshtastic.core.ui.theme.StatusColors.StatusRed
private const val FORMAT = "%d%%"
private const val SIZE_ICON = 16
@Suppress("MagicNumber", "LongMethod")
@ -60,7 +59,7 @@ fun MaterialBatteryInfo(
voltage: Float? = null,
contentColor: Color = MaterialTheme.colorScheme.onSurface,
) {
val levelString = formatString(FORMAT, level)
val levelString = level?.let { MetricFormatter.percent(it) } ?: stringResource(Res.string.unknown)
Row(
modifier = modifier,
@ -130,7 +129,7 @@ fun MaterialBatteryInfo(
?.takeIf { it > 0 }
?.let {
Text(
text = formatString("%.2fV", it),
text = MetricFormatter.voltage(it),
color = contentColor.copy(alpha = 0.8f),
style = MaterialTheme.typography.labelMedium.copy(fontSize = 12.sp),
)

View file

@ -34,7 +34,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.resources.vectorResource
import org.meshtastic.core.common.util.formatString
import org.meshtastic.core.common.util.MetricFormatter
import org.meshtastic.core.model.Node
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.signal_quality
@ -65,7 +65,10 @@ fun SignalInfo(
tint = signalColor,
)
Text(
text = formatString("%.1fdB · %ddBm · %s", node.snr, node.rssi, stringResource(quality.nameRes)),
text =
"${MetricFormatter.snr(
node.snr,
)} · ${MetricFormatter.rssi(node.rssi)} · ${stringResource(quality.nameRes)}",
style =
MaterialTheme.typography.labelSmall.copy(
fontWeight = FontWeight.Bold,

View file

@ -59,6 +59,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
@ -117,8 +118,8 @@ fun EmojiPickerDialog(
onConfirm: (String) -> Unit,
) {
val viewModel: EmojiPickerViewModel = koinViewModel()
var searchQuery by remember { mutableStateOf("") }
var selectedCategoryIndex by remember { mutableStateOf(0) }
var searchQuery by rememberSaveable { mutableStateOf("") }
var selectedCategoryIndex by rememberSaveable { mutableStateOf(0) }
val recentEmojis by
remember(viewModel.customEmojiFrequency) { derivedStateOf { parseRecents(viewModel.customEmojiFrequency) } }
@ -427,7 +428,7 @@ private fun SectionHeader(title: String) {
@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun EmojiCellWithSkinTone(emoji: Emoji, isSelected: Boolean, onSelect: (String) -> Unit) {
var showSkinTonePopup by remember { mutableStateOf(false) }
var showSkinTonePopup by rememberSaveable { mutableStateOf(false) }
Box {
Box(

View file

@ -40,6 +40,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
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
@ -89,7 +90,7 @@ fun ScannedQrCodeDialog(
onDismiss: () -> Unit,
onConfirm: (ChannelSet) -> Unit,
) {
var shouldReplace by remember { mutableStateOf(incoming.lora_config != null) }
var shouldReplace by rememberSaveable { mutableStateOf(incoming.lora_config != null) }
val channelSet =
remember(shouldReplace, channels, incoming) {

View file

@ -21,7 +21,6 @@ package org.meshtastic.core.ui.util
import androidx.compose.runtime.Composable
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.MeshtasticUri
/** Returns a function to open the platform's NFC settings. */
@Composable expect fun rememberOpenNfcSettings(): () -> Unit
@ -41,7 +40,7 @@ import org.meshtastic.core.common.util.MeshtasticUri
/** Returns a launcher function to prompt the user to save a file. The callback receives the saved file URI. */
@Composable
expect fun rememberSaveFileLauncher(
onUriReceived: (MeshtasticUri) -> Unit,
onUriReceived: (CommonUri) -> Unit,
): (defaultFilename: String, mimeType: String) -> Unit
/** Returns a launcher function to prompt the user to open/pick a file. The callback receives the selected file URI. */

View file

@ -36,7 +36,6 @@ import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.MeshtasticUri
import org.meshtastic.core.database.entity.asDeviceVersion
import org.meshtastic.core.model.MeshActivity
import org.meshtastic.core.model.MyNodeInfo
@ -99,18 +98,16 @@ class UIViewModel(
* 2. **Data Import:** If navigation fails, falls back to legacy contact/channel parsing via
* [dispatchMeshtasticUri]. This triggers import dialogs for shared nodes or channel configurations.
*/
fun handleDeepLink(uri: MeshtasticUri, onInvalid: () -> Unit = {}) {
val commonUri = CommonUri.parse(uri.uriString)
fun handleDeepLink(uri: CommonUri, onInvalid: () -> Unit = {}) {
// Try navigation routing first
val navKeys = DeepLinkRouter.route(commonUri)
val navKeys = DeepLinkRouter.route(uri)
if (navKeys != null) {
_navigationDeepLink.tryEmit(navKeys)
return
}
// Fallback to channel/contact importing
commonUri.dispatchMeshtasticUri(
uri.dispatchMeshtasticUri(
onContact = { setSharedContactRequested(it) },
onChannel = { setRequestChannelSet(it) },
onInvalid = onInvalid,

View file

@ -21,6 +21,7 @@ package org.meshtastic.core.ui.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@ -37,7 +38,6 @@ import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.unknown_error
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds

View file

@ -22,7 +22,6 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextLinkStyles
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.MeshtasticUri
actual fun createClipEntry(text: String, label: String): ClipEntry =
throw UnsupportedOperationException("ClipEntry instantiation not supported on iOS stub")
@ -41,7 +40,7 @@ actual fun annotatedStringFromHtml(html: String, linkStyles: TextLinkStyles?): A
@Composable
actual fun rememberSaveFileLauncher(
onUriReceived: (MeshtasticUri) -> Unit,
onUriReceived: (CommonUri) -> Unit,
): (defaultFilename: String, mimeType: String) -> Unit = { _, _ -> }
@Composable

View file

@ -24,7 +24,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.common.util.MeshtasticUri
import java.awt.Desktop
import java.awt.FileDialog
import java.awt.Frame
@ -61,7 +60,7 @@ actual fun rememberOpenUrl(): (url: String) -> Unit = { url ->
/** JVM — Opens a native file dialog to save a file. */
@Composable
actual fun rememberSaveFileLauncher(
onUriReceived: (MeshtasticUri) -> Unit,
onUriReceived: (CommonUri) -> Unit,
): (defaultFilename: String, mimeType: String) -> Unit = { defaultFilename, _ ->
val dialog = FileDialog(null as Frame?, "Save File", FileDialog.SAVE)
dialog.file = defaultFilename
@ -70,7 +69,7 @@ actual fun rememberSaveFileLauncher(
val dir = dialog.directory
if (file != null && dir != null) {
val path = File(dir, file)
onUriReceived(MeshtasticUri(path.toURI().toString()))
onUriReceived(CommonUri.parse(path.toURI().toString()))
}
}
@ -83,7 +82,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
val dir = dialog.directory
if (file != null && dir != null) {
val path = File(dir, file)
onUriReceived(CommonUri(path.toURI()))
onUriReceived(CommonUri.parse(path.toURI().toString()))
}
}