mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-15 19:38:15 -04:00
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:
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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?) {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user