refactor(viewmodels): adopt safeLaunch across 13 ViewModels

Replace raw viewModelScope.launch with safeLaunch() context receiver
across all feature ViewModels for consistent error handling:

- MessageViewModel, QuickChatViewModel, ContactsViewModel (messaging)
- MetricsViewModel, CompassViewModel (node)
- SettingsViewModel, RadioConfigViewModel, CleanNodeDatabaseViewModel,
  DebugViewModel, ChannelViewModel (settings)
- BaseMapViewModel (map)
- ScannerViewModel (connections)
- ScannedQrCodeViewModel (core/ui)

Add optional dispatcher parameter to safeLaunch() for ViewModels that
need ioDispatcher (e.g. database operations in MessageViewModel).

RadioConfigViewModel: only import/export/install methods converted;
ResponseState<T> config request flows left as-is due to specialized
progress tracking requirements.
This commit is contained in:
James Rich
2026-04-13 15:28:09 -05:00
parent fd15957c04
commit b87c9528fa
14 changed files with 104 additions and 100 deletions

View File

@@ -17,12 +17,11 @@
package org.meshtastic.core.ui.qr
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.util.getChannelList
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
@@ -40,7 +39,7 @@ class ScannedQrCodeViewModel(
private val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
/** Set the radio config (also updates our saved copy in preferences). */
fun setChannels(channelSet: ChannelSet) = viewModelScope.launch {
fun setChannels(channelSet: ChannelSet) = safeLaunch(tag = "setChannels") {
getChannelList(channelSet.settings, channels.value.settings).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(channelSet.settings)
@@ -51,11 +50,11 @@ class ScannedQrCodeViewModel(
}
private fun setChannel(channel: Channel) {
viewModelScope.launch { radioController.setLocalChannel(channel) }
safeLaunch(tag = "setChannel") { radioController.setLocalChannel(channel) }
}
// Set the radio config (also updates our saved copy in preferences)
private fun setConfig(config: Config) {
viewModelScope.launch { radioController.setLocalConfig(config) }
safeLaunch(tag = "setConfig") { radioController.setLocalConfig(config) }
}
}

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.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
@@ -102,18 +103,22 @@ fun <T> Flow<T>.asUiState(stopTimeout: Duration = 5.seconds): StateFlow<UiState<
*/
context(viewModel: ViewModel)
fun safeLaunch(
dispatcher: CoroutineDispatcher? = null,
errorEvents: MutableSharedFlow<UiText>? = null,
tag: String? = null,
block: suspend CoroutineScope.() -> Unit,
): Job = viewModel.viewModelScope.launch {
try {
block()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
val label = tag ?: "safeLaunch"
Logger.e(e) { "[$label] Unhandled exception" }
errorEvents?.emit(UiText.DynamicString(e.message ?: "Unknown error"))
): Job {
val context = dispatcher ?: viewModel.viewModelScope.coroutineContext
return viewModel.viewModelScope.launch(context) {
try {
block()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
val label = tag ?: "safeLaunch"
Logger.e(e) { "[$label] Unhandled exception" }
errorEvents?.emit(UiText.DynamicString(e.message ?: "Unknown error"))
}
}
}

View File

@@ -37,6 +37,7 @@ import org.meshtastic.core.model.util.anonymize
import org.meshtastic.core.repository.RadioInterfaceService
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.connections.model.DeviceListEntry
import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase
@@ -185,11 +186,11 @@ open class ScannerViewModel(
fun addRecentAddress(address: String, name: String) {
if (!address.startsWith("t")) return
viewModelScope.launch { recentAddressesDataSource.add(RecentAddress(address, name)) }
safeLaunch(tag = "addRecentAddress") { recentAddressesDataSource.add(RecentAddress(address, name)) }
}
fun removeRecentAddress(address: String) {
viewModelScope.launch { recentAddressesDataSource.remove(address) }
safeLaunch(tag = "removeRecentAddress") { recentAddressesDataSource.remove(address) }
}
/**
@@ -221,7 +222,7 @@ open class ScannerViewModel(
}
}
is DeviceListEntry.Tcp -> {
viewModelScope.launch {
safeLaunch(tag = "onSelectedTcp") {
radioPrefs.setDevName(it.name)
addRecentAddress(it.fullAddress, it.name)
changeDeviceAddress(it.fullAddress)

View File

@@ -17,14 +17,12 @@
package org.meshtastic.feature.map
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.common.util.nowSeconds
@@ -41,6 +39,7 @@ import org.meshtastic.core.resources.eight_hours
import org.meshtastic.core.resources.one_day
import org.meshtastic.core.resources.one_hour
import org.meshtastic.core.resources.two_days
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.Position
import org.meshtastic.proto.Waypoint
@@ -147,7 +146,8 @@ open class BaseMapViewModel(
fun getNodeOrFallback(nodeNum: Int): Node = nodeRepository.nodeDBbyNum.value[nodeNum] ?: Node(num = nodeNum)
fun deleteWaypoint(id: Int) = viewModelScope.launch(ioDispatcher) { packetRepository.deleteWaypoint(id) }
fun deleteWaypoint(id: Int) =
safeLaunch(dispatcher = ioDispatcher, tag = "deleteWaypoint") { packetRepository.deleteWaypoint(id) }
fun sendWaypoint(wpt: Waypoint, contactKey: String = "0${DataPacket.ID_BROADCAST}") {
// contactKey: unique contact key filter (channel)+(nodeId)
@@ -159,7 +159,7 @@ open class BaseMapViewModel(
}
private fun sendDataPacket(p: DataPacket) {
viewModelScope.launch(ioDispatcher) { radioController.sendMessage(p) }
safeLaunch(dispatcher = ioDispatcher, tag = "sendDataPacket") { radioController.sendMessage(p) }
}
fun generatePacketId(): Int = radioController.getPacketId()

View File

@@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.ContactSettings
@@ -49,6 +48,7 @@ import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
@@ -157,7 +157,7 @@ class MessageViewModel(
}
fun setTitle(title: String) {
viewModelScope.launch { _title.value = title }
safeLaunch(tag = "setTitle") { _title.value = title }
}
fun getMessagesFromPaged(contactKey: String): Flow<PagingData<Message>> {
@@ -190,7 +190,9 @@ class MessageViewModel(
}
fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) {
viewModelScope.launch(ioDispatcher) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
safeLaunch(dispatcher = ioDispatcher, tag = "setContactFilteringDisabled") {
packetRepository.setContactFilteringDisabled(contactKey, disabled)
}
}
fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
@@ -211,21 +213,21 @@ class MessageViewModel(
* @param replyId The ID of the message this is a reply to, if any.
*/
fun sendMessage(str: String, contactKey: String = "0${DataPacket.ID_BROADCAST}", replyId: Int? = null) {
viewModelScope.launch { sendMessageUseCase.invoke(str, contactKey, replyId) }
safeLaunch(tag = "sendMessage") { sendMessageUseCase.invoke(str, contactKey, replyId) }
}
fun sendReaction(emoji: String, replyId: Int, contactKey: String) = viewModelScope.launch {
fun sendReaction(emoji: String, replyId: Int, contactKey: String) = safeLaunch(tag = "sendReaction") {
serviceRepository.onServiceAction(ServiceAction.Reaction(emoji, replyId, contactKey))
}
fun deleteMessages(uuidList: List<Long>) =
viewModelScope.launch(ioDispatcher) { packetRepository.deleteMessages(uuidList) }
safeLaunch(dispatcher = ioDispatcher, tag = "deleteMessages") { packetRepository.deleteMessages(uuidList) }
fun clearUnreadCount(contact: String, messageUuid: Long, lastReadTimestamp: Long) =
viewModelScope.launch(ioDispatcher) {
safeLaunch(dispatcher = ioDispatcher, tag = "clearUnreadCount") {
val existingTimestamp = contactSettings.value[contact]?.lastReadMessageTimestamp ?: Long.MIN_VALUE
if (lastReadTimestamp <= existingTimestamp) {
return@launch
return@safeLaunch
}
packetRepository.clearUnreadCount(contact, lastReadTimestamp)
packetRepository.updateLastReadMessage(contact, messageUuid, lastReadTimestamp)

View File

@@ -17,12 +17,11 @@
package org.meshtastic.feature.messaging
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.database.entity.QuickChatAction
import org.meshtastic.core.repository.QuickChatActionRepository
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
@KoinViewModel
@@ -31,7 +30,7 @@ class QuickChatViewModel(private val quickChatActionRepository: QuickChatActionR
get() = quickChatActionRepository.getAllActions().stateInWhileSubscribed(initialValue = emptyList())
fun updateActionPositions(actions: List<QuickChatAction>) {
viewModelScope.launch(ioDispatcher) {
safeLaunch(dispatcher = ioDispatcher, tag = "updateActionPositions") {
for (position in actions.indices) {
quickChatActionRepository.setItemPosition(actions[position].uuid, position)
}
@@ -39,8 +38,10 @@ class QuickChatViewModel(private val quickChatActionRepository: QuickChatActionR
}
fun addQuickChatAction(action: QuickChatAction) =
viewModelScope.launch(ioDispatcher) { quickChatActionRepository.upsert(action) }
safeLaunch(dispatcher = ioDispatcher, tag = "addQuickChatAction") { quickChatActionRepository.upsert(action) }
fun deleteQuickChatAction(action: QuickChatAction) =
viewModelScope.launch(ioDispatcher) { quickChatActionRepository.delete(action) }
safeLaunch(dispatcher = ioDispatcher, tag = "deleteQuickChatAction") {
quickChatActionRepository.delete(action)
}
}

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.ioDispatcher
import org.meshtastic.core.model.Contact
@@ -37,6 +36,7 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
import kotlin.collections.map as collectionsMap
@@ -188,17 +188,20 @@ class ContactsViewModel(
fun getNode(userId: String?) = nodeRepository.getNode(userId ?: DataPacket.ID_BROADCAST)
fun deleteContacts(contacts: List<String>) =
viewModelScope.launch(ioDispatcher) { packetRepository.deleteContacts(contacts) }
safeLaunch(dispatcher = ioDispatcher, tag = "deleteContacts") { packetRepository.deleteContacts(contacts) }
fun markAllAsRead() = viewModelScope.launch(ioDispatcher) { packetRepository.clearAllUnreadCounts() }
fun markAllAsRead() =
safeLaunch(dispatcher = ioDispatcher, tag = "markAllAsRead") { packetRepository.clearAllUnreadCounts() }
fun setMuteUntil(contacts: List<String>, until: Long) =
viewModelScope.launch(ioDispatcher) { packetRepository.setMuteUntil(contacts, until) }
safeLaunch(dispatcher = ioDispatcher, tag = "setMuteUntil") { packetRepository.setMuteUntil(contacts, until) }
fun getContactSettings() = packetRepository.getContactSettings()
fun setContactFilteringDisabled(contactKey: String, disabled: Boolean) {
viewModelScope.launch(ioDispatcher) { packetRepository.setContactFilteringDisabled(contactKey, disabled) }
safeLaunch(dispatcher = ioDispatcher, tag = "setContactFilteringDisabled") {
packetRepository.setContactFilteringDisabled(contactKey, disabled)
}
}
/**

View File

@@ -18,7 +18,6 @@ package org.meshtastic.feature.node.compass
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -26,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.bearing
import org.meshtastic.core.common.util.formatString
@@ -37,6 +35,7 @@ import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.toDistanceString
import org.meshtastic.core.ui.component.precisionBitsToMeters
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.proto.Config
import org.meshtastic.proto.Position
import kotlin.math.abs
@@ -92,13 +91,17 @@ class CompassViewModel(
updatesJob?.cancel()
updatesJob = viewModelScope.launch {
combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) { heading, location ->
buildState(heading, location)
updatesJob =
safeLaunch(tag = "compassUpdates") {
combine(headingProvider.headingUpdates(), phoneLocationProvider.locationUpdates()) {
heading,
location,
->
buildState(heading, location)
}
.flowOn(dispatchers.default)
.collect { _uiState.value = it }
}
.flowOn(dispatchers.default)
.collect { _uiState.value = it }
}
}
fun stop() {

View File

@@ -31,7 +31,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
import okio.ByteString.Companion.decodeBase64
@@ -60,6 +59,7 @@ import org.meshtastic.core.resources.traceroute
import org.meshtastic.core.resources.view_on_map
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.util.toMessageRes
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.feature.node.detail.NodeRequestActions
import org.meshtastic.feature.node.domain.usecase.GetNodeDetailsUseCase
@@ -181,7 +181,8 @@ open class MetricsViewModel(
fun getUser(nodeNum: Int) = nodeRepository.getUser(nodeNum)
fun deleteLog(uuid: String) = viewModelScope.launch(dispatchers.io) { meshLogRepository.deleteLog(uuid) }
fun deleteLog(uuid: String) =
safeLaunch(dispatcher = dispatchers.io, tag = "deleteLog") { meshLogRepository.deleteLog(uuid) }
fun getTracerouteOverlay(requestId: Int): TracerouteOverlay? {
val cached = tracerouteOverlayCache.value[requestId]
@@ -216,7 +217,7 @@ open class MetricsViewModel(
private fun List<Node>.numSet(): Set<Int> = map { it.num }.toSet()
init {
viewModelScope.launch {
safeLaunch(tag = "tracerouteCollector") {
serviceRepository.tracerouteResponse.filterNotNull().collect { response ->
val overlay =
TracerouteOverlay(
@@ -232,7 +233,7 @@ open class MetricsViewModel(
Logger.d { "MetricsViewModel created" }
}
fun clearPosition() = viewModelScope.launch(dispatchers.io) {
fun clearPosition() = safeLaunch(dispatcher = dispatchers.io, tag = "clearPosition") {
(manualNodeId.value ?: nodeIdFromRoute)?.let {
meshLogRepository.deleteLogs(it, PortNum.POSITION_APP.value)
}
@@ -276,7 +277,7 @@ open class MetricsViewModel(
overlay: TracerouteOverlay?,
onViewOnMap: (Int, String) -> Unit,
) {
viewModelScope.launch {
safeLaunch(tag = "showTracerouteDetail") {
val snapshotPositions = tracerouteSnapshotRepository.getSnapshotPositions(responseLogUuid).first()
alertManager.showAlert(
titleRes = Res.string.traceroute,
@@ -299,7 +300,7 @@ open class MetricsViewModel(
if (errorRes != null) {
// Post the error alert after the current alert is dismissed to avoid
// the wrapping dismissAlert() in AlertManager immediately clearing it.
viewModelScope.launch {
safeLaunch(tag = "tracerouteError") {
alertManager.showAlert(titleRes = Res.string.traceroute, messageRes = errorRes)
}
} else {
@@ -336,7 +337,7 @@ open class MetricsViewModel(
epochSeconds: (T) -> Long,
rowMapper: (T) -> String,
) {
viewModelScope.launch(dispatchers.io) {
safeLaunch(dispatcher = dispatchers.io, tag = "exportCsv") {
fileService.write(uri) { sink ->
sink.writeUtf8(header)
rows.forEach { item ->

View File

@@ -17,7 +17,6 @@
package org.meshtastic.feature.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@@ -25,7 +24,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okio.BufferedSink
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.BuildConfigProvider
@@ -51,6 +49,7 @@ import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.NotificationPrefs
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
@@ -146,12 +145,12 @@ class SettingsViewModel(
val meshLogLoggingEnabled: StateFlow<Boolean> = _meshLogLoggingEnabled.asStateFlow()
fun setMeshLogRetentionDays(days: Int) {
viewModelScope.launch { setMeshLogSettingsUseCase.setRetentionDays(days) }
safeLaunch(tag = "setMeshLogRetentionDays") { setMeshLogSettingsUseCase.setRetentionDays(days) }
_meshLogRetentionDays.value = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS)
}
fun setMeshLogLoggingEnabled(enabled: Boolean) {
viewModelScope.launch { setMeshLogSettingsUseCase.setLoggingEnabled(enabled) }
safeLaunch(tag = "setMeshLogLoggingEnabled") { setMeshLogSettingsUseCase.setLoggingEnabled(enabled) }
_meshLogLoggingEnabled.value = enabled
}
@@ -183,7 +182,9 @@ class SettingsViewModel(
* @param filterPortnum If provided, only packets with this port number will be exported.
*/
fun saveDataCsv(uri: MeshtasticUri, filterPortnum: Int? = null) {
viewModelScope.launch { fileService.write(uri) { writer -> performDataExport(writer, filterPortnum) } }
safeLaunch(tag = "saveDataCsv") {
fileService.write(uri) { writer -> performDataExport(writer, filterPortnum) }
}
}
private suspend fun performDataExport(writer: BufferedSink, filterPortnum: Int?) {

View File

@@ -17,11 +17,9 @@
package org.meshtastic.feature.settings.channel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.CommonUri
import org.meshtastic.core.model.RadioController
@@ -30,6 +28,7 @@ import org.meshtastic.core.repository.DataPair
import org.meshtastic.core.repository.PlatformAnalytics
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.ui.util.getChannelList
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.Channel
import org.meshtastic.proto.ChannelSet
@@ -86,7 +85,7 @@ class ChannelViewModel(
}
/** Set the radio config (also updates our saved copy in preferences). */
fun setChannels(channelSet: ChannelSet) = viewModelScope.launch {
fun setChannels(channelSet: ChannelSet) = safeLaunch(tag = "setChannels") {
getChannelList(channelSet.settings, channels.value.settings).forEach(::setChannel)
radioConfigRepository.replaceAllSettings(channelSet.settings)
@@ -97,12 +96,12 @@ class ChannelViewModel(
}
fun setChannel(channel: Channel) {
viewModelScope.launch { radioController.setLocalChannel(channel) }
safeLaunch(tag = "setChannel") { radioController.setLocalChannel(channel) }
}
// Set the radio config (also updates our saved copy in preferences)
fun setConfig(config: Config) {
viewModelScope.launch { radioController.setLocalConfig(config) }
safeLaunch(tag = "setConfig") { radioController.setLocalConfig(config) }
}
fun trackShare() {

View File

@@ -18,7 +18,6 @@ package org.meshtastic.feature.settings.debugging
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import co.touchlab.kermit.Logger
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -29,7 +28,6 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.DateFormatter
@@ -47,6 +45,7 @@ import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_clear
import org.meshtastic.core.resources.debug_clear_logs_confirm
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.AdminMessage
import org.meshtastic.proto.MeshPacket
@@ -265,16 +264,18 @@ class DebugViewModel(
val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS)
meshLogPrefs.setRetentionDays(clamped)
_retentionDays.value = clamped
viewModelScope.launch { meshLogRepository.deleteLogsOlderThan(clamped) }
safeLaunch(tag = "setRetentionDays") { meshLogRepository.deleteLogsOlderThan(clamped) }
}
fun setLoggingEnabled(enabled: Boolean) {
meshLogPrefs.setLoggingEnabled(enabled)
_loggingEnabled.value = enabled
if (!enabled) {
viewModelScope.launch { meshLogRepository.deleteAll() }
safeLaunch(tag = "disableLogging") { meshLogRepository.deleteAll() }
} else {
viewModelScope.launch { meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) }
safeLaunch(tag = "enableLogging") {
meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value)
}
}
}
@@ -286,7 +287,7 @@ class DebugViewModel(
init {
Logger.d { "DebugViewModel created" }
viewModelScope.launch {
safeLaunch(tag = "searchMatchUpdater") {
combine(searchManager.searchText, filterManager.filteredLogs) { searchText, logs ->
searchManager.findSearchMatches(searchText, logs)
}
@@ -406,7 +407,7 @@ class DebugViewModel(
)
}
fun deleteAllLogs() = viewModelScope.launch(ioDispatcher) { meshLogRepository.deleteAll() }
fun deleteAllLogs() = safeLaunch(dispatcher = ioDispatcher, tag = "deleteAllLogs") { meshLogRepository.deleteAll() }
@Immutable
data class UiMeshLog(

View File

@@ -17,10 +17,8 @@
package org.meshtastic.feature.settings.radio
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.getString
import org.koin.core.annotation.KoinViewModel
import org.meshtastic.core.common.util.nowSeconds
@@ -31,6 +29,7 @@ import org.meshtastic.core.resources.are_you_sure
import org.meshtastic.core.resources.clean_node_database_confirmation
import org.meshtastic.core.resources.clean_now
import org.meshtastic.core.ui.util.AlertManager
import org.meshtastic.core.ui.viewmodel.safeLaunch
private const val MIN_DAYS_THRESHOLD = 7f
@@ -65,7 +64,7 @@ class CleanNodeDatabaseViewModel(
/** Updates the list of nodes to be deleted based on the current filter criteria. */
fun getNodesToDelete() {
viewModelScope.launch {
safeLaunch(tag = "getNodesToDelete") {
_nodesToDelete.value =
cleanNodeDatabaseUseCase.getNodesToClean(
olderThanDays = _olderThanDays.value,
@@ -76,7 +75,7 @@ class CleanNodeDatabaseViewModel(
}
fun requestCleanNodes() {
viewModelScope.launch {
safeLaunch(tag = "requestCleanNodes") {
val count = _nodesToDelete.value.size
val message = getString(Res.string.clean_node_database_confirmation, count)
alertManager.showAlert(
@@ -93,7 +92,7 @@ class CleanNodeDatabaseViewModel(
* them.
*/
fun cleanNodes() {
viewModelScope.launch {
safeLaunch(tag = "cleanNodes") {
val nodeNums = _nodesToDelete.value.map { it.num }
cleanNodeDatabaseUseCase.cleanNodes(nodeNums)
// Clear the list after deletion or if it was empty

View File

@@ -62,6 +62,7 @@ import org.meshtastic.core.resources.UiText
import org.meshtastic.core.resources.cant_shutdown
import org.meshtastic.core.resources.timeout
import org.meshtastic.core.ui.util.getChannelList
import org.meshtastic.core.ui.viewmodel.safeLaunch
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.feature.settings.navigation.ModuleRoute
import org.meshtastic.proto.AdminMessage
@@ -385,46 +386,34 @@ open class RadioConfigViewModel(
}
fun importProfile(uri: MeshtasticUri, onResult: (DeviceProfile) -> Unit) {
viewModelScope.launch {
try {
var profile: DeviceProfile? = null
fileService.read(uri) { source ->
importProfileUseCase(source).onSuccess { profile = it }.onFailure { throw it }
}
profile?.let { onResult(it) }
} catch (ex: Exception) {
Logger.e { "Import DeviceProfile error: ${ex.message}" }
safeLaunch(tag = "importProfile") {
var profile: DeviceProfile? = null
fileService.read(uri) { source ->
importProfileUseCase(source).onSuccess { profile = it }.onFailure { throw it }
}
profile?.let { onResult(it) }
}
}
fun exportProfile(uri: MeshtasticUri, profile: DeviceProfile) {
viewModelScope.launch {
try {
fileService.write(uri) { sink ->
exportProfileUseCase(sink, profile).onSuccess { /* Success */ }.onFailure { throw it }
}
} catch (ex: Exception) {
Logger.e { "Can't write file error: ${ex.message}" }
safeLaunch(tag = "exportProfile") {
fileService.write(uri) { sink ->
exportProfileUseCase(sink, profile).onSuccess { /* Success */ }.onFailure { throw it }
}
}
}
fun exportSecurityConfig(uri: MeshtasticUri, securityConfig: Config.SecurityConfig) {
viewModelScope.launch {
try {
fileService.write(uri) { sink ->
exportSecurityConfigUseCase(sink, securityConfig).onSuccess { /* Success */ }.onFailure { throw it }
}
} catch (ex: Exception) {
Logger.e { "Can't write security keys JSON error: ${ex.message}" }
safeLaunch(tag = "exportSecurityConfig") {
fileService.write(uri) { sink ->
exportSecurityConfigUseCase(sink, securityConfig).onSuccess { /* Success */ }.onFailure { throw it }
}
}
}
fun installProfile(protobuf: DeviceProfile) {
val destNum = destNode.value?.num ?: return
viewModelScope.launch { installProfileUseCase(destNum, protobuf, destNode.value?.user) }
safeLaunch(tag = "installProfile") { installProfileUseCase(destNum, protobuf, destNode.value?.user) }
}
fun clearPacketResponse() {