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

@ -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