refactor: migrate preferences to DataStore and decouple core:domain for KMP (#4731)

This commit is contained in:
James Rich 2026-03-05 20:37:35 -06:00 committed by GitHub
parent 87fdaa26ff
commit b9b68d2779
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
113 changed files with 1790 additions and 1320 deletions

View file

@ -20,10 +20,10 @@ import android.net.Uri
import kotlinx.coroutines.flow.Flow
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.prefs.radio.isBle
import org.meshtastic.core.prefs.radio.isSerial
import org.meshtastic.core.prefs.radio.isTcp
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.isBle
import org.meshtastic.core.repository.isSerial
import org.meshtastic.core.repository.isTcp
import org.meshtastic.feature.firmware.ota.Esp32OtaUpdateHandler
import java.io.File
import javax.inject.Inject
@ -90,7 +90,7 @@ constructor(
private fun getTarget(address: String): String = when {
radioPrefs.isSerial() -> ""
radioPrefs.isBle() -> address
radioPrefs.isTcp() -> extractIpFromAddress(radioPrefs.devAddr) ?: ""
radioPrefs.isTcp() -> extractIpFromAddress(radioPrefs.devAddr.value) ?: ""
else -> ""
}

View file

@ -45,12 +45,12 @@ import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.radio.RadioPrefs
import org.meshtastic.core.prefs.radio.isBle
import org.meshtastic.core.prefs.radio.isSerial
import org.meshtastic.core.prefs.radio.isTcp
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioPrefs
import org.meshtastic.core.repository.isBle
import org.meshtastic.core.repository.isSerial
import org.meshtastic.core.repository.isTcp
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.firmware_update_battery_low
import org.meshtastic.core.resources.firmware_update_copying
@ -157,7 +157,7 @@ constructor(
_state.value = FirmwareUpdateState.Checking
runCatching {
val ourNode = nodeRepository.myNodeInfo.value
val address = radioPrefs.devAddr?.drop(1)
val address = radioPrefs.devAddr.value?.drop(1)
if (address == null || ourNode == null) {
_state.value = FirmwareUpdateState.Error(getString(Res.string.firmware_update_no_device))
return@launch

View file

@ -26,7 +26,7 @@ import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
@ -52,9 +52,9 @@ constructor(
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
var mapStyleId: Int
get() = mapPrefs.mapStyle
get() = mapPrefs.mapStyle.value
set(value) {
mapPrefs.mapStyle = value
mapPrefs.setMapStyle(value)
}
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())

View file

@ -49,7 +49,7 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.navigation.MapRoutes
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
@ -96,9 +96,9 @@ constructor(
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
private val targetLatLng =
googleMapsPrefs.cameraTargetLat
googleMapsPrefs.cameraTargetLat.value
.takeIf { it != 0.0 }
?.let { lat -> googleMapsPrefs.cameraTargetLng.takeIf { it != 0.0 }?.let { lng -> LatLng(lat, lng) } }
?.let { lat -> googleMapsPrefs.cameraTargetLng.value.takeIf { it != 0.0 }?.let { lng -> LatLng(lat, lng) } }
?: ourNodeInfo.value?.position?.toLatLng()
?: LatLng(0.0, 0.0)
@ -107,9 +107,9 @@ constructor(
position =
CameraPosition(
targetLatLng,
googleMapsPrefs.cameraZoom,
googleMapsPrefs.cameraTilt,
googleMapsPrefs.cameraBearing,
googleMapsPrefs.cameraZoom.value,
googleMapsPrefs.cameraTilt.value,
googleMapsPrefs.cameraBearing.value,
),
)
@ -222,7 +222,7 @@ constructor(
) {
_selectedCustomTileProviderUrl.value = null
// Also clear from prefs
googleMapsPrefs.selectedCustomTileUrl = null
googleMapsPrefs.setSelectedCustomTileUrl(null)
}
if (configToRemove.localUri != null) {
@ -238,28 +238,28 @@ constructor(
if (!config.isLocal && !isValidTileUrlTemplate(config.urlTemplate)) {
Logger.withTag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
_selectedCustomTileProviderUrl.value = null
googleMapsPrefs.selectedCustomTileUrl = null
googleMapsPrefs.setSelectedCustomTileUrl(null)
return
}
// Use localUri if present, otherwise urlTemplate
val selectedUrl = config.localUri ?: config.urlTemplate
_selectedCustomTileProviderUrl.value = selectedUrl
_selectedGoogleMapType.value = MapType.NONE
googleMapsPrefs.selectedCustomTileUrl = selectedUrl
googleMapsPrefs.selectedGoogleMapType = null
googleMapsPrefs.setSelectedCustomTileUrl(selectedUrl)
googleMapsPrefs.setSelectedGoogleMapType(null)
} else {
_selectedCustomTileProviderUrl.value = null
_selectedGoogleMapType.value = MapType.NORMAL
googleMapsPrefs.selectedCustomTileUrl = null
googleMapsPrefs.selectedGoogleMapType = MapType.NORMAL.name
googleMapsPrefs.setSelectedCustomTileUrl(null)
googleMapsPrefs.setSelectedGoogleMapType(MapType.NORMAL.name)
}
}
fun setSelectedGoogleMapType(mapType: MapType) {
_selectedGoogleMapType.value = mapType
_selectedCustomTileProviderUrl.value = null // Clear custom selection
googleMapsPrefs.selectedGoogleMapType = mapType.name
googleMapsPrefs.selectedCustomTileUrl = null
googleMapsPrefs.setSelectedGoogleMapType(mapType.name)
googleMapsPrefs.setSelectedCustomTileUrl(null)
}
private var currentTileProvider: TileProvider? = null
@ -354,16 +354,16 @@ constructor(
fun saveCameraPosition(cameraPosition: CameraPosition) {
viewModelScope.launch {
googleMapsPrefs.cameraTargetLat = cameraPosition.target.latitude
googleMapsPrefs.cameraTargetLng = cameraPosition.target.longitude
googleMapsPrefs.cameraZoom = cameraPosition.zoom
googleMapsPrefs.cameraTilt = cameraPosition.tilt
googleMapsPrefs.cameraBearing = cameraPosition.bearing
googleMapsPrefs.setCameraTargetLat(cameraPosition.target.latitude)
googleMapsPrefs.setCameraTargetLng(cameraPosition.target.longitude)
googleMapsPrefs.setCameraZoom(cameraPosition.zoom)
googleMapsPrefs.setCameraTilt(cameraPosition.tilt)
googleMapsPrefs.setCameraBearing(cameraPosition.bearing)
}
}
private fun loadPersistedMapType() {
val savedCustomUrl = googleMapsPrefs.selectedCustomTileUrl
val savedCustomUrl = googleMapsPrefs.selectedCustomTileUrl.value
if (savedCustomUrl != null) {
// Check if this custom provider still exists
if (
@ -375,18 +375,18 @@ constructor(
MapType.NONE // MapType.NONE to hide google basemap when using custom provider
} else {
// The saved custom URL is no longer valid or doesn't exist, remove preference
googleMapsPrefs.selectedCustomTileUrl = null
googleMapsPrefs.setSelectedCustomTileUrl(null)
// Fallback to default Google Map type
_selectedGoogleMapType.value = MapType.NORMAL
}
} else {
val savedGoogleMapTypeName = googleMapsPrefs.selectedGoogleMapType
val savedGoogleMapTypeName = googleMapsPrefs.selectedGoogleMapType.value
try {
_selectedGoogleMapType.value = MapType.valueOf(savedGoogleMapTypeName ?: MapType.NORMAL.name)
} catch (e: IllegalArgumentException) {
Logger.e(e) { "Invalid saved Google Map type: $savedGoogleMapTypeName" }
_selectedGoogleMapType.value = MapType.NORMAL // Fallback in case of invalid stored name
googleMapsPrefs.selectedGoogleMapType = null
googleMapsPrefs.setSelectedGoogleMapType(null)
}
}
}
@ -399,7 +399,7 @@ constructor(
val persistedLayerFiles = layersDir.listFiles()
if (persistedLayerFiles != null) {
val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls
val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls.value
val loadedItems =
persistedLayerFiles.mapNotNull { file ->
if (file.isFile) {
@ -429,7 +429,7 @@ constructor(
}
val networkItems =
googleMapsPrefs.networkMapLayers.mapNotNull { networkString ->
googleMapsPrefs.networkMapLayers.value.mapNotNull { networkString ->
try {
val parts = networkString.split("|:|")
if (parts.size == 3) {
@ -532,7 +532,7 @@ constructor(
_mapLayers.value = _mapLayers.value + newItem
val networkLayerString = "${newItem.id}|:|${newItem.name}|:|${newItem.uri}"
googleMapsPrefs.networkMapLayers = googleMapsPrefs.networkMapLayers + networkLayerString
googleMapsPrefs.setNetworkMapLayers(googleMapsPrefs.networkMapLayers.value + networkLayerString)
} catch (e: Exception) {
_errorFlow.emit("Invalid URL.")
}
@ -572,9 +572,9 @@ constructor(
toggledLayer?.let {
if (it.isVisible) {
googleMapsPrefs.hiddenLayerUrls -= it.uri.toString()
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value - it.uri.toString())
} else {
googleMapsPrefs.hiddenLayerUrls += it.uri.toString()
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value + it.uri.toString())
}
}
}
@ -584,12 +584,13 @@ constructor(
val layerToRemove = _mapLayers.value.find { it.id == layerId }
layerToRemove?.uri?.let { uri ->
if (layerToRemove.isNetwork) {
googleMapsPrefs.networkMapLayers =
googleMapsPrefs.networkMapLayers.filterNot { it.startsWith("$layerId|:|") }.toSet()
googleMapsPrefs.setNetworkMapLayers(
googleMapsPrefs.networkMapLayers.value.filterNot { it.startsWith("$layerId|:|") }.toSet(),
)
} else {
deleteFileToInternalStorage(uri)
}
googleMapsPrefs.hiddenLayerUrls -= uri.toString()
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value - uri.toString())
}
_mapLayers.value = _mapLayers.value.filterNot { it.id == layerId }
}

View file

@ -30,7 +30,7 @@ import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.resources.Res
@ -90,47 +90,48 @@ abstract class BaseMapViewModel(
}
.stateInWhileSubscribed(initialValue = emptyMap())
private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites)
private val showOnlyFavorites = MutableStateFlow(mapPrefs.showOnlyFavorites.value)
val showOnlyFavoritesOnMap = showOnlyFavorites
fun toggleOnlyFavorites() {
val newValue = !showOnlyFavorites.value
showOnlyFavorites.value = newValue
mapPrefs.showOnlyFavorites = newValue
mapPrefs.setShowOnlyFavorites(newValue)
}
private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap)
private val showWaypoints = MutableStateFlow(mapPrefs.showWaypointsOnMap.value)
val showWaypointsOnMap = showWaypoints
fun toggleShowWaypointsOnMap() {
val newValue = !showWaypoints.value
showWaypoints.value = newValue
mapPrefs.showWaypointsOnMap = newValue
mapPrefs.setShowWaypointsOnMap(newValue)
}
private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap)
private val showPrecisionCircle = MutableStateFlow(mapPrefs.showPrecisionCircleOnMap.value)
val showPrecisionCircleOnMap = showPrecisionCircle
fun toggleShowPrecisionCircleOnMap() {
val newValue = !showPrecisionCircle.value
showPrecisionCircle.value = newValue
mapPrefs.showPrecisionCircleOnMap = newValue
mapPrefs.setShowPrecisionCircleOnMap(newValue)
}
private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter))
private val lastHeardFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardFilter.value))
val lastHeardFilter = lastHeardFilterValue
fun setLastHeardFilter(filter: LastHeardFilter) {
lastHeardFilterValue.value = filter
mapPrefs.lastHeardFilter = filter.seconds
mapPrefs.setLastHeardFilter(filter.seconds)
}
private val lastHeardTrackFilterValue = MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter))
private val lastHeardTrackFilterValue =
MutableStateFlow(LastHeardFilter.fromSeconds(mapPrefs.lastHeardTrackFilter.value))
val lastHeardTrackFilter = lastHeardTrackFilterValue
fun setLastHeardTrackFilter(filter: LastHeardFilter) {
lastHeardTrackFilterValue.value = filter
mapPrefs.lastHeardTrackFilter = filter.seconds
mapPrefs.setLastHeardTrackFilter(filter.seconds)
}
abstract fun getUser(userId: String?): org.meshtastic.proto.User

View file

@ -28,10 +28,10 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.toList
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.ui.util.toPosition
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
@ -81,5 +81,5 @@ constructor(
.stateInWhileSubscribed(initialValue = emptyList())
val tileSource
get() = CustomTileSource.getTileSource(mapPrefs.mapStyle)
get() = CustomTileSource.getTileSource(mapPrefs.mapStyle.value)
}

View file

@ -44,7 +44,7 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.ConnectionState
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
import org.meshtastic.core.prefs.map.MapPrefs
import org.meshtastic.core.repository.MapPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
@ -72,6 +72,22 @@ class MapViewModelTest {
@Before
fun setup() {
Dispatchers.setMain(testDispatcher)
every { mapPrefs.mapStyle } returns MutableStateFlow(0)
every { mapPrefs.showOnlyFavorites } returns MutableStateFlow(false)
every { mapPrefs.showWaypointsOnMap } returns MutableStateFlow(true)
every { mapPrefs.showPrecisionCircleOnMap } returns MutableStateFlow(true)
every { mapPrefs.lastHeardFilter } returns MutableStateFlow(0L)
every { mapPrefs.lastHeardTrackFilter } returns MutableStateFlow(0L)
every { googleMapsPrefs.cameraTargetLat } returns MutableStateFlow(0.0)
every { googleMapsPrefs.cameraTargetLng } returns MutableStateFlow(0.0)
every { googleMapsPrefs.cameraZoom } returns MutableStateFlow(0f)
every { googleMapsPrefs.cameraTilt } returns MutableStateFlow(0f)
every { googleMapsPrefs.cameraBearing } returns MutableStateFlow(0f)
every { googleMapsPrefs.selectedCustomTileUrl } returns MutableStateFlow(null)
every { googleMapsPrefs.selectedGoogleMapType } returns MutableStateFlow(null)
every { googleMapsPrefs.hiddenLayerUrls } returns MutableStateFlow(emptySet())
every { customTileProviderRepository.getCustomTileProviders() } returns flowOf(emptyList())
every { radioConfigRepository.deviceProfileFlow } returns flowOf(mockk(relaxed = true))
every { uiPreferencesDataSource.theme } returns MutableStateFlow(1)

View file

@ -38,14 +38,14 @@ import org.meshtastic.core.model.DataPacket
import org.meshtastic.core.model.Message
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.service.ServiceAction
import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.repository.CustomEmojiPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MeshServiceNotifications
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.repository.UiPrefs
import org.meshtastic.core.repository.usecase.SendMessageUseCase
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.ChannelSet
@ -79,7 +79,7 @@ constructor(
val channels = radioConfigRepository.channelSetFlow.stateInWhileSubscribed(ChannelSet())
private val _showQuickChat = MutableStateFlow(uiPrefs.showQuickChat)
private val _showQuickChat = MutableStateFlow(uiPrefs.showQuickChat.value)
val showQuickChat: StateFlow<Boolean> = _showQuickChat
private val _showFiltered = MutableStateFlow(false)
@ -109,7 +109,7 @@ constructor(
val frequentEmojis: List<String>
get() =
customEmojiPrefs.customEmojiFrequency
customEmojiPrefs.customEmojiFrequency.value
?.split(",")
?.associate { entry ->
entry.split("=", limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1].toInt() } ?: ("" to 0)
@ -119,7 +119,7 @@ constructor(
?.map { it.first }
?.take(6) ?: listOf("👍", "👎", "😂", "🔥", "❤️", "😮")
val homoglyphEncodingEnabled = homoglyphEncodingPrefs.getHomoglyphEncodingEnabledChangesFlow()
val homoglyphEncodingEnabled = homoglyphEncodingPrefs.homoglyphEncodingEnabled
val firstUnreadMessageUuid: StateFlow<Long?> =
contactKeyForPagedMessages
@ -163,7 +163,7 @@ constructor(
return pagedMessagesForContactKey
}
fun toggleShowQuickChat() = toggle(_showQuickChat) { uiPrefs.showQuickChat = it }
fun toggleShowQuickChat() = toggle(_showQuickChat) { uiPrefs.setShowQuickChat(it) }
fun toggleShowFiltered() {
_showFiltered.update { !it }

View file

@ -24,7 +24,6 @@ import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onStart
import org.meshtastic.core.data.repository.FirmwareReleaseRepository
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.FirmwareRelease
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.model.MyNodeInfo
@ -32,6 +31,7 @@ import org.meshtastic.core.model.Node
import org.meshtastic.core.model.util.hasValidEnvironmentMetrics
import org.meshtastic.core.model.util.isDirectSignal
import org.meshtastic.core.repository.DeviceHardwareRepository
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.resources.Res

View file

@ -45,7 +45,6 @@ import org.jetbrains.compose.resources.StringResource
import org.meshtastic.core.common.util.nowSeconds
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.di.CoroutineDispatchers
@ -54,6 +53,7 @@ import org.meshtastic.core.model.TelemetryType
import org.meshtastic.core.model.evaluateTracerouteMapAvailability
import org.meshtastic.core.model.util.UnitConversions
import org.meshtastic.core.navigation.NodesRoutes
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.resources.Res
@ -134,7 +134,7 @@ constructor(
val availableTimeFrames: StateFlow<List<TimeFrame>> =
combine(state, environmentState) { currentState, envState ->
val stateOldest = currentState.oldestTimestampSeconds()
val envOldest = envState.environmentMetrics.minOfOrNull { (it.time ?: 0).toLong() }?.takeIf { it > 0 }
val envOldest = envState.environmentMetrics.minOfOrNull { it.time.toLong() }?.takeIf { it > 0 }
val oldest = listOfNotNull(stateOldest, envOldest).minOrNull() ?: nowSeconds
TimeFrame.entries.filter { it.isAvailable(oldest) }
}
@ -148,7 +148,7 @@ constructor(
val filteredEnvironmentMetrics: StateFlow<List<Telemetry>> =
combine(environmentState, _timeFrame, state) { envState, timeFrame, currentState ->
val threshold = timeFrame.timeThreshold()
val data = envState.environmentMetrics.filter { (it.time ?: 0).toLong() >= threshold }
val data = envState.environmentMetrics.filter { it.time.toLong() >= threshold }
if (currentState.isFahrenheit) {
data.map { telemetry ->
val em = telemetry.environment_metrics ?: return@map telemetry
@ -341,7 +341,7 @@ constructor(
val dateFormat = SimpleDateFormat("\"yyyy-MM-dd\",\"HH:mm:ss\"", Locale.getDefault())
positions.forEach { position ->
val rxDateTime = dateFormat.format(((position.time ?: 0).toLong() * 1000L).toInstant().toDate())
val rxDateTime = dateFormat.format((position.time.toLong() * 1000L).toInstant().toDate())
val latitude = (position.latitude_i ?: 0) * 1e-7
val longitude = (position.longitude_i ?: 0) * 1e-7
val altitude = position.altitude
@ -377,7 +377,7 @@ constructor(
if (packet != null && decoded != null && decoded.portnum == PortNum.PAXCOUNTER_APP) {
if (decoded.want_response == true) return null
val pax = ProtoPaxcount.ADAPTER.decode(decoded.payload)
if ((pax.ble ?: 0) != 0 || (pax.wifi ?: 0) != 0 || (pax.uptime ?: 0) != 0) return pax
if (pax.ble != 0 || pax.wifi != 0 || pax.uptime != 0) return pax
}
} catch (e: IOException) {
Logger.e(e) { "Failed to parse Paxcount from binary data" }

View file

@ -32,6 +32,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
@ -43,11 +44,10 @@ import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.repository.DatabaseManager
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed
import org.meshtastic.proto.LocalConfig
import java.io.BufferedWriter
@ -126,10 +126,10 @@ constructor(
}
// MeshLog retention period (bounded by MeshLogPrefsImpl constants)
private val _meshLogRetentionDays = MutableStateFlow(meshLogPrefs.retentionDays)
private val _meshLogRetentionDays = MutableStateFlow(meshLogPrefs.retentionDays.value)
val meshLogRetentionDays: StateFlow<Int> = _meshLogRetentionDays.asStateFlow()
private val _meshLogLoggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled)
private val _meshLogLoggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled.value)
val meshLogLoggingEnabled: StateFlow<Boolean> = _meshLogLoggingEnabled.asStateFlow()
fun setMeshLogRetentionDays(days: Int) {

View file

@ -36,13 +36,13 @@ import kotlinx.coroutines.withContext
import org.meshtastic.core.common.util.nowInstant
import org.meshtastic.core.common.util.toDate
import org.meshtastic.core.common.util.toInstant
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.database.entity.MeshLog
import org.meshtastic.core.database.entity.Packet
import org.meshtastic.core.model.getTracerouteResponse
import org.meshtastic.core.model.util.decodeOrNull
import org.meshtastic.core.model.util.toReadableString
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.resources.Res
import org.meshtastic.core.resources.debug_clear
@ -230,10 +230,10 @@ constructor(
.mapLatest { logs -> withContext(Dispatchers.Default) { toUiState(logs) } }
.stateInWhileSubscribed(initialValue = persistentListOf())
private val _retentionDays = MutableStateFlow(meshLogPrefs.retentionDays)
private val _retentionDays = MutableStateFlow(meshLogPrefs.retentionDays.value)
val retentionDays: StateFlow<Int> = _retentionDays.asStateFlow()
private val _loggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled)
private val _loggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled.value)
val loggingEnabled: StateFlow<Boolean> = _loggingEnabled.asStateFlow()
// --- Managers ---
@ -265,18 +265,18 @@ constructor(
fun setRetentionDays(days: Int) {
val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS)
meshLogPrefs.retentionDays = clamped
meshLogPrefs.setRetentionDays(clamped)
_retentionDays.value = clamped
viewModelScope.launch { meshLogRepository.deleteLogsOlderThan(clamped) }
}
fun setLoggingEnabled(enabled: Boolean) {
meshLogPrefs.loggingEnabled = enabled
meshLogPrefs.setLoggingEnabled(enabled)
_loggingEnabled.value = enabled
if (!enabled) {
viewModelScope.launch { meshLogRepository.deleteAll() }
} else {
viewModelScope.launch { meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays) }
viewModelScope.launch { meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) }
}
}

View file

@ -21,7 +21,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import org.meshtastic.core.prefs.filter.FilterPrefs
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
import javax.inject.Inject
@ -33,32 +33,32 @@ constructor(
private val messageFilter: MessageFilter,
) : ViewModel() {
private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled)
private val _filterEnabled = MutableStateFlow(filterPrefs.filterEnabled.value)
val filterEnabled: StateFlow<Boolean> = _filterEnabled.asStateFlow()
private val _filterWords = MutableStateFlow(filterPrefs.filterWords.toList().sorted())
private val _filterWords = MutableStateFlow(filterPrefs.filterWords.value.toList().sorted())
val filterWords: StateFlow<List<String>> = _filterWords.asStateFlow()
fun setFilterEnabled(enabled: Boolean) {
filterPrefs.filterEnabled = enabled
filterPrefs.setFilterEnabled(enabled)
_filterEnabled.value = enabled
}
fun addFilterWord(word: String) {
if (word.isBlank()) return
val trimmed = word.trim()
val current = filterPrefs.filterWords.toMutableSet()
val current = filterPrefs.filterWords.value.toMutableSet()
if (current.add(trimmed)) {
filterPrefs.filterWords = current
filterPrefs.setFilterWords(current)
_filterWords.value = current.toList().sorted()
messageFilter.rebuildPatterns()
}
}
fun removeFilterWord(word: String) {
val current = filterPrefs.filterWords.toMutableSet()
val current = filterPrefs.filterWords.value.toMutableSet()
if (current.remove(word)) {
filterPrefs.filterWords = current
filterPrefs.setFilterWords(current)
_filterWords.value = current.toList().sorted()
messageFilter.rebuildPatterns()
}

View file

@ -58,9 +58,9 @@ import org.meshtastic.core.model.MyNodeInfo
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.Position
import org.meshtastic.core.navigation.SettingsRoutes
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MapConsentPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
@ -131,13 +131,13 @@ constructor(
private val adminActionsUseCase: AdminActionsUseCase,
private val processRadioResponseUseCase: ProcessRadioResponseUseCase,
) : ViewModel() {
var analyticsAllowedFlow = analyticsPrefs.getAnalyticsAllowedChangesFlow()
var analyticsAllowedFlow = analyticsPrefs.analyticsAllowed
fun toggleAnalyticsAllowed() {
toggleAnalyticsUseCase()
}
val homoglyphEncodingEnabledFlow = homoglyphEncodingPrefs.getHomoglyphEncodingEnabledChangesFlow()
val homoglyphEncodingEnabledFlow = homoglyphEncodingPrefs.homoglyphEncodingEnabled
fun toggleHomoglyphCharactersEncodingEnabled() {
toggleHomoglyphEncodingUseCase()

View file

@ -61,7 +61,8 @@ fun MQTTConfigScreen(viewModel: RadioConfigViewModel = hiltViewModel(), onBack:
val currentMapReportSettings = formState.value.map_report_settings ?: ModuleConfig.MapReportSettings()
if (!(currentMapReportSettings.should_report_location ?: false)) {
val settings = currentMapReportSettings.copy(should_report_location = viewModel.shouldReportLocation(destNum))
val settings =
currentMapReportSettings.copy(should_report_location = viewModel.shouldReportLocation(destNum).value)
formState.value = formState.value.copy(map_report_settings = settings)
}

View file

@ -30,6 +30,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.common.BuildConfigProvider
import org.meshtastic.core.common.database.DatabaseManager
import org.meshtastic.core.domain.usecase.settings.ExportDataUseCase
import org.meshtastic.core.domain.usecase.settings.IsOtaCapableUseCase
import org.meshtastic.core.domain.usecase.settings.MeshLocationUseCase
@ -39,11 +40,10 @@ import org.meshtastic.core.domain.usecase.settings.SetMeshLogSettingsUseCase
import org.meshtastic.core.domain.usecase.settings.SetProvideLocationUseCase
import org.meshtastic.core.domain.usecase.settings.SetThemeUseCase
import org.meshtastic.core.model.RadioController
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.repository.DatabaseManager
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.UiPrefs
import org.robolectric.annotation.Config
@OptIn(ExperimentalCoroutinesApi::class)

View file

@ -32,8 +32,8 @@ import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.data.repository.MeshLogRepository
import org.meshtastic.core.prefs.meshlog.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogPrefs
import org.meshtastic.core.repository.MeshLogRepository
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.ui.util.AlertManager
@ -56,8 +56,8 @@ class DebugViewModelTest {
every { meshLogRepository.getAllLogs() } returns flowOf(emptyList())
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())
every { meshLogPrefs.retentionDays } returns 7
every { meshLogPrefs.loggingEnabled } returns true
every { meshLogPrefs.retentionDays.value } returns 7
every { meshLogPrefs.loggingEnabled.value } returns true
viewModel =
DebugViewModel(
@ -77,7 +77,7 @@ class DebugViewModelTest {
fun `setRetentionDays updates prefs and deletes old logs`() = runTest {
viewModel.setRetentionDays(14)
verify { meshLogPrefs.retentionDays = 14 }
verify { meshLogPrefs.setRetentionDays(14) }
coVerify { meshLogRepository.deleteLogsOlderThan(14) }
assertEquals(14, viewModel.retentionDays.value)
}
@ -86,7 +86,7 @@ class DebugViewModelTest {
fun `setLoggingEnabled false deletes all logs`() = runTest {
viewModel.setLoggingEnabled(false)
verify { meshLogPrefs.loggingEnabled = false }
verify { meshLogPrefs.setLoggingEnabled(false) }
coVerify { meshLogRepository.deleteAll() }
assertEquals(false, viewModel.loggingEnabled.value)
}

View file

@ -22,7 +22,7 @@ import io.mockk.verify
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.meshtastic.core.prefs.filter.FilterPrefs
import org.meshtastic.core.repository.FilterPrefs
import org.meshtastic.core.repository.MessageFilter
class FilterSettingsViewModelTest {
@ -34,8 +34,8 @@ class FilterSettingsViewModelTest {
@Before
fun setUp() {
every { filterPrefs.filterEnabled } returns true
every { filterPrefs.filterWords } returns setOf("apple", "banana")
every { filterPrefs.filterEnabled.value } returns true
every { filterPrefs.filterWords.value } returns setOf("apple", "banana")
viewModel = FilterSettingsViewModel(filterPrefs = filterPrefs, messageFilter = messageFilter)
}
@ -43,7 +43,7 @@ class FilterSettingsViewModelTest {
@Test
fun `setFilterEnabled updates prefs and state`() {
viewModel.setFilterEnabled(false)
verify { filterPrefs.filterEnabled = false }
verify { filterPrefs.setFilterEnabled(false) }
assertEquals(false, viewModel.filterEnabled.value)
}
@ -51,7 +51,7 @@ class FilterSettingsViewModelTest {
fun `addFilterWord updates prefs and rebuilds patterns`() {
viewModel.addFilterWord("cherry")
verify { filterPrefs.filterWords = any() }
verify { filterPrefs.setFilterWords(any()) }
verify { messageFilter.rebuildPatterns() }
assertEquals(listOf("apple", "banana", "cherry"), viewModel.filterWords.value)
}
@ -60,7 +60,7 @@ class FilterSettingsViewModelTest {
fun `removeFilterWord updates prefs and rebuilds patterns`() {
viewModel.removeFilterWord("apple")
verify { filterPrefs.filterWords = any() }
verify { filterPrefs.setFilterWords(any()) }
verify { messageFilter.rebuildPatterns() }
assertEquals(listOf("banana"), viewModel.filterWords.value)
}

View file

@ -45,9 +45,9 @@ import org.meshtastic.core.domain.usecase.settings.RadioResponseResult
import org.meshtastic.core.domain.usecase.settings.ToggleAnalyticsUseCase
import org.meshtastic.core.domain.usecase.settings.ToggleHomoglyphEncodingUseCase
import org.meshtastic.core.model.Node
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs
import org.meshtastic.core.prefs.map.MapConsentPrefs
import org.meshtastic.core.repository.AnalyticsPrefs
import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.MapConsentPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository