From b9b68d2779236464d89d12b29bf55b159b3df453 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Thu, 5 Mar 2026 20:37:35 -0600 Subject: [PATCH] refactor: migrate preferences to DataStore and decouple core:domain for KMP (#4731) --- .../filter/MessageFilterIntegrationTest.kt | 6 +- .../geeksville/mesh/MeshUtilApplication.kt | 6 +- .../com/geeksville/mesh/model/UIViewModel.kt | 2 +- .../radio/AndroidRadioInterfaceService.kt | 8 +- .../ui/connections/ConnectionsViewModel.kt | 6 +- .../mesh/worker/MeshLogCleanupWorker.kt | 12 +- core/analytics/build.gradle.kts | 1 + .../platform/GooglePlatformAnalytics.kt | 9 +- .../core/common/database}/DatabaseManager.kt | 2 +- core/data/build.gradle.kts | 2 + .../CustomTileProviderRepository.kt | 9 +- .../core/data}/di/DataStoreModule.kt | 37 +++- .../meshtastic/core/data/di/DatabaseModule.kt | 8 +- .../core/data/di/RepositoryModule.kt | 6 + .../core/data/manager/HistoryManagerImpl.kt | 8 +- .../data/manager/MeshActionHandlerImpl.kt | 10 +- .../data/manager/MeshConnectionManagerImpl.kt | 2 +- .../data/manager/MeshMessageProcessorImpl.kt | 2 +- .../core/data/manager/MessageFilterImpl.kt | 6 +- .../core/data/manager/PacketHandlerImpl.kt | 2 +- ...Repository.kt => MeshLogRepositoryImpl.kt} | 48 ++--- .../manager/MeshConnectionManagerImplTest.kt | 2 +- .../data/manager/MessageFilterImplTest.kt | 19 +- .../data/manager/PacketHandlerImplTest.kt | 2 +- .../data/repository/MeshLogRepositoryTest.kt | 8 +- core/database/build.gradle.kts | 2 +- .../core/database/DatabaseManager.kt | 2 +- core/datastore/build.gradle.kts | 49 ++--- .../datastore/BootloaderWarningDataSource.kt | 3 +- .../core/datastore/ChannelSetDataSource.kt | 0 .../core/datastore/LocalConfigDataSource.kt | 0 .../core/datastore/LocalStatsDataSource.kt | 0 .../core/datastore/ModuleConfigDataSource.kt | 0 .../datastore/RecentAddressesDataSource.kt | 3 +- .../core/datastore/UiPreferencesDataSource.kt | 19 +- .../core/datastore/model/RecentAddress.kt | 3 +- .../serializer/ChannelSetSerializer.kt | 17 +- .../serializer/LocalConfigSerializer.kt | 17 +- .../serializer/LocalStatsSerializer.kt | 17 +- .../serializer/ModuleConfigSerializer.kt | 18 +- core/domain/build.gradle.kts | 2 - .../usecase/settings/ExportDataUseCase.kt | 2 +- .../usecase/settings/IsOtaCapableUseCase.kt | 8 +- .../settings/SetDatabaseCacheLimitUseCase.kt | 2 +- .../settings/SetMeshLogSettingsUseCase.kt | 10 +- .../settings/SetProvideLocationUseCase.kt | 2 +- .../settings/ToggleAnalyticsUseCase.kt | 4 +- .../ToggleHomoglyphEncodingUseCase.kt | 4 +- .../domain/usecase/SendMessageUseCaseTest.kt | 8 +- .../usecase/settings/ExportDataUseCaseTest.kt | 2 +- .../settings/IsOtaCapableUseCaseTest.kt | 8 +- .../SetDatabaseCacheLimitUseCaseTest.kt | 2 +- .../settings/SetMeshLogSettingsUseCaseTest.kt | 12 +- .../settings/SetProvideLocationUseCaseTest.kt | 2 +- .../settings/ToggleAnalyticsUseCaseTest.kt | 10 +- .../ToggleHomoglyphEncodingUseCaseTest.kt | 10 +- core/prefs/build.gradle.kts | 5 + .../core/prefs/di/GoogleMapsModule.kt | 28 ++- .../core/prefs/map/GoogleMapsPrefs.kt | 181 ++++++++++++++--- .../core/prefs/DoublePrefDelegate.kt | 39 ---- .../core/prefs/FloatPrefDelegate.kt | 34 ---- .../core/prefs/NullableStringPrefDelegate.kt | 48 ----- .../org/meshtastic/core/prefs/PrefDelegate.kt | 61 ------ .../core/prefs/StringSetPrefDelegate.kt | 35 ---- .../core/prefs/analytics/AnalyticsPrefs.kt | 82 -------- .../prefs/analytics/AnalyticsPrefsImpl.kt | 78 ++++++++ .../meshtastic/core/prefs/di/PrefsModule.kt | 188 +++++++++++------- .../core/prefs/emoji/CustomEmojiPrefs.kt | 34 ---- .../core/prefs/emoji/CustomEmojiPrefsImpl.kt | 64 ++++++ .../core/prefs/filter/FilterPrefs.kt | 50 ----- .../core/prefs/filter/FilterPrefsImpl.kt | 70 +++++++ .../core/prefs/homoglyph/HomoglyphPrefs.kt | 68 ------- .../prefs/homoglyph/HomoglyphPrefsImpl.kt | 56 ++++++ .../core/prefs/map/MapConsentPrefs.kt | 40 ---- .../core/prefs/map/MapConsentPrefsImpl.kt | 56 ++++++ .../org/meshtastic/core/prefs/map/MapPrefs.kt | 44 ---- .../meshtastic/core/prefs/map/MapPrefsImpl.kt | 97 +++++++++ .../core/prefs/map/MapTileProviderPrefs.kt | 34 ---- .../prefs/map/MapTileProviderPrefsImpl.kt | 64 ++++++ .../meshtastic/core/prefs/mesh/MeshPrefs.kt | 77 ------- .../core/prefs/mesh/MeshPrefsImpl.kt | 114 +++++++++++ .../core/prefs/meshlog/MeshLogPrefs.kt | 56 ------ .../core/prefs/meshlog/MeshLogPrefsImpl.kt | 73 +++++++ .../meshtastic/core/prefs/radio/RadioPrefs.kt | 43 ---- .../core/prefs/radio/RadioPrefsImpl.kt | 63 ++++++ .../org/meshtastic/core/prefs/ui/UiPrefs.kt | 77 ------- .../meshtastic/core/prefs/ui/UiPrefsImpl.kt | 81 ++++++++ .../core/prefs/filter/FilterPrefsTest.kt | 70 ++++--- core/repository/build.gradle.kts | 1 + .../core/repository/AppPreferences.kt | 173 ++++++++++++++++ .../core/repository/HomoglyphPrefs.kt | 21 -- .../core/repository/MeshLogRepository.kt | 82 ++++++++ .../repository/usecase/SendMessageUseCase.kt | 2 +- .../core/ui/emoji/EmojiPickerViewModel.kt | 9 +- .../feature/firmware/FirmwareUpdateManager.kt | 10 +- .../firmware/FirmwareUpdateViewModel.kt | 10 +- .../meshtastic/feature/map/MapViewModel.kt | 6 +- .../meshtastic/feature/map/MapViewModel.kt | 63 +++--- .../feature/map/BaseMapViewModel.kt | 23 ++- .../feature/map/node/NodeMapViewModel.kt | 6 +- .../feature/map/MapViewModelTest.kt | 18 +- .../feature/messaging/MessageViewModel.kt | 14 +- .../domain/usecase/GetNodeDetailsUseCase.kt | 2 +- .../feature/node/metrics/MetricsViewModel.kt | 10 +- .../feature/settings/SettingsViewModel.kt | 10 +- .../settings/debugging/DebugViewModel.kt | 14 +- .../filter/FilterSettingsViewModel.kt | 16 +- .../settings/radio/RadioConfigViewModel.kt | 10 +- .../radio/component/MQTTConfigItemList.kt | 3 +- .../feature/settings/SettingsViewModelTest.kt | 6 +- .../settings/debugging/DebugViewModelTest.kt | 12 +- .../filter/FilterSettingsViewModelTest.kt | 12 +- .../radio/RadioConfigViewModelTest.kt | 6 +- 113 files changed, 1790 insertions(+), 1320 deletions(-) rename core/{repository/src/commonMain/kotlin/org/meshtastic/core/repository => common/src/commonMain/kotlin/org/meshtastic/core/common/database}/DatabaseManager.kt (96%) rename core/{datastore/src/main/kotlin/org/meshtastic/core/datastore => data/src/main/kotlin/org/meshtastic/core/data}/di/DataStoreModule.kt (83%) rename core/data/src/main/kotlin/org/meshtastic/core/data/repository/{MeshLogRepository.kt => MeshLogRepositoryImpl.kt} (80%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt (98%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt (100%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt (100%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt (100%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt (100%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt (99%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt (90%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt (95%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt (72%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt (72%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt (72%) rename core/datastore/src/{main => commonMain}/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt (71%) delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/DoublePrefDelegate.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/FloatPrefDelegate.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/NullableStringPrefDelegate.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/PrefDelegate.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/StringSetPrefDelegate.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt delete mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt create mode 100644 core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt delete mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt create mode 100644 core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt diff --git a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt index 2c327a7af..efa229881 100644 --- a/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt +++ b/app/src/androidTest/java/com/geeksville/mesh/filter/MessageFilterIntegrationTest.kt @@ -25,7 +25,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.prefs.filter.FilterPrefs +import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter import javax.inject.Inject @@ -46,8 +46,8 @@ class MessageFilterIntegrationTest { @Test fun filterPrefsIntegration() = runTest { - filterPrefs.filterEnabled = true - filterPrefs.filterWords = setOf("test", "spam") + filterPrefs.setFilterEnabled(true) + filterPrefs.setFilterWords(setOf("test", "spam")) filterService.rebuildPatterns() assertTrue(filterService.shouldFilter("this is a test message")) diff --git a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt index 9843c49f9..6e1573f2d 100644 --- a/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt +++ b/app/src/main/java/com/geeksville/mesh/MeshUtilApplication.kt @@ -44,8 +44,8 @@ import kotlinx.coroutines.withTimeout import no.nordicsemi.kotlin.ble.core.android.AndroidEnvironment import org.meshtastic.core.common.ContextServices import org.meshtastic.core.database.DatabaseManager -import org.meshtastic.core.prefs.mesh.MeshPrefs -import org.meshtastic.core.prefs.meshlog.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.MeshPrefs import javax.inject.Inject import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds @@ -114,7 +114,7 @@ open class MeshUtilApplication : // Initialize DatabaseManager asynchronously with current device address so DAO consumers have an active DB val entryPoint = EntryPointAccessors.fromApplication(this, AppEntryPoint::class.java) - applicationScope.launch { entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress) } + applicationScope.launch { entryPoint.databaseManager().init(entryPoint.meshPrefs().deviceAddress.value) } } override fun onTerminate() { diff --git a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt index a3511ca74..77a6cde1f 100644 --- a/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/model/UIViewModel.kt @@ -42,7 +42,6 @@ import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.getString import org.meshtastic.core.analytics.platform.PlatformAnalytics import org.meshtastic.core.data.repository.FirmwareReleaseRepository -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.asDeviceVersion import org.meshtastic.core.datastore.UiPreferencesDataSource import org.meshtastic.core.model.MeshActivity @@ -52,6 +51,7 @@ import org.meshtastic.core.model.TracerouteMapAvailability import org.meshtastic.core.model.evaluateTracerouteMapAvailability import org.meshtastic.core.model.service.TracerouteResponse import org.meshtastic.core.model.util.dispatchMeshtasticUri +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository diff --git a/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt index 47230a08a..bab2fc843 100644 --- a/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt +++ b/app/src/main/java/com/geeksville/mesh/repository/radio/AndroidRadioInterfaceService.kt @@ -51,8 +51,8 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.InterfaceId import org.meshtastic.core.model.MeshActivity import org.meshtastic.core.model.util.anonymize -import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.RadioPrefs import org.meshtastic.proto.Heartbeat import org.meshtastic.proto.ToRadio import javax.inject.Inject @@ -92,7 +92,7 @@ constructor( val connectionError: SharedFlow = _connectionError.asSharedFlow() // Thread-safe StateFlow for tracking device address changes - private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr) + private val _currentDeviceAddressFlow = MutableStateFlow(radioPrefs.devAddr.value) override val currentDeviceAddressFlow: StateFlow = _currentDeviceAddressFlow.asStateFlow() private val logSends = false @@ -192,7 +192,7 @@ constructor( */ override fun getDeviceAddress(): String? { // If the user has unpaired our device, treat things as if we don't have one - var address = radioPrefs.devAddr + var address = radioPrefs.devAddr.value // If we are running on the emulator we default to the mock interface, so we can have some data to show to the // user @@ -352,7 +352,7 @@ constructor( Logger.d { "Setting bonded device to ${address.anonymize}" } // Stores the address if non-null, otherwise removes the pref - radioPrefs.devAddr = address + radioPrefs.setDevAddr(address) _currentDeviceAddressFlow.value = address // Force the service to reconnect diff --git a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt index b17281ff6..e7a363725 100644 --- a/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt +++ b/app/src/main/java/com/geeksville/mesh/ui/connections/ConnectionsViewModel.kt @@ -23,10 +23,10 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node -import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.ui.viewmodel.stateInWhileSubscribed import org.meshtastic.proto.LocalConfig import javax.inject.Inject @@ -50,11 +50,11 @@ constructor( val ourNodeInfo: StateFlow = nodeRepository.ourNodeInfo - private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning) + private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning.value) val hasShownNotPairedWarning: StateFlow = _hasShownNotPairedWarning.asStateFlow() fun suppressNoPairedWarning() { _hasShownNotPairedWarning.value = true - uiPrefs.hasShownNotPairedWarning = true + uiPrefs.setHasShownNotPairedWarning(true) } } diff --git a/app/src/main/java/com/geeksville/mesh/worker/MeshLogCleanupWorker.kt b/app/src/main/java/com/geeksville/mesh/worker/MeshLogCleanupWorker.kt index 72f11ce87..d84e961bd 100644 --- a/app/src/main/java/com/geeksville/mesh/worker/MeshLogCleanupWorker.kt +++ b/app/src/main/java/com/geeksville/mesh/worker/MeshLogCleanupWorker.kt @@ -27,8 +27,8 @@ import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.android.EntryPointAccessors import dagger.hilt.components.SingletonComponent -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 @HiltWorker class MeshLogCleanupWorker @@ -53,14 +53,14 @@ constructor( @Suppress("TooGenericExceptionCaught") override suspend fun doWork(): Result = try { - val retentionDays = meshLogPrefs.retentionDays - if (!meshLogPrefs.loggingEnabled) { + val retentionDays = meshLogPrefs.retentionDays.value + if (!meshLogPrefs.loggingEnabled.value) { logger.i { "Skipping cleanup because mesh log storage is disabled" } - } else if (retentionDays == MeshLogPrefs.NEVER_CLEAR_RETENTION_DAYS) { + } else if (retentionDays == 0) { logger.i { "Skipping cleanup because retention is set to never delete" } } else { val retentionLabel = - if (retentionDays == MeshLogPrefs.ONE_HOUR_RETENTION_DAYS) { + if (retentionDays == -1) { "1 hour" } else { "$retentionDays days" diff --git a/core/analytics/build.gradle.kts b/core/analytics/build.gradle.kts index efb532fcf..5ee46fe82 100644 --- a/core/analytics/build.gradle.kts +++ b/core/analytics/build.gradle.kts @@ -27,6 +27,7 @@ plugins { dependencies { implementation(projects.core.prefs) + implementation(projects.core.repository) implementation(libs.androidx.compose.runtime) implementation(libs.androidx.lifecycle.process) diff --git a/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt index 7bd13f840..c3133b8f4 100644 --- a/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt +++ b/core/analytics/src/google/kotlin/org/meshtastic/core/analytics/platform/GooglePlatformAnalytics.kt @@ -58,7 +58,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.meshtastic.core.analytics.BuildConfig import org.meshtastic.core.analytics.DataPair -import org.meshtastic.core.prefs.analytics.AnalyticsPrefs +import org.meshtastic.core.repository.AnalyticsPrefs import javax.inject.Inject import co.touchlab.kermit.Logger as KermitLogger @@ -109,11 +109,10 @@ constructor( KermitLogger.setMinSeverity(if (BuildConfig.DEBUG) Severity.Debug else Severity.Info) // Initial consent state - updateAnalyticsConsent(analyticsPrefs.analyticsAllowed) + updateAnalyticsConsent(analyticsPrefs.analyticsAllowed.value) // Subscribe to analytics preference changes - analyticsPrefs - .getAnalyticsAllowedChangesFlow() + analyticsPrefs.analyticsAllowed .onEach { allowed -> updateAnalyticsConsent(allowed) } .launchIn(ProcessLifecycleOwner.get().lifecycleScope) } @@ -122,7 +121,7 @@ constructor( * Ensures that Datadog and Firebase SDKs are initialized if allowed. This is called lazily when consent is granted. */ private fun ensureInitialized() { - if (!analyticsPrefs.analyticsAllowed || isInTestLab) return + if (!analyticsPrefs.analyticsAllowed.value || isInTestLab) return if (!Datadog.isInitialized()) { initDatadog(context as Application) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/database/DatabaseManager.kt similarity index 96% rename from core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt rename to core/common/src/commonMain/kotlin/org/meshtastic/core/common/database/DatabaseManager.kt index 675092382..86cc549b0 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/DatabaseManager.kt +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/database/DatabaseManager.kt @@ -14,7 +14,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.repository +package org.meshtastic.core.common.database import kotlinx.coroutines.flow.StateFlow diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index 1279e4a5c..6da9b686c 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -31,6 +31,8 @@ dependencies { implementation(projects.core.common) implementation(projects.core.database) implementation(projects.core.datastore) + implementation(libs.androidx.datastore) + implementation(libs.androidx.datastore.preferences) implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.network) diff --git a/core/data/src/google/kotlin/org/meshtastic/core/data/repository/CustomTileProviderRepository.kt b/core/data/src/google/kotlin/org/meshtastic/core/data/repository/CustomTileProviderRepository.kt index 9ce615f53..5fbe32d92 100644 --- a/core/data/src/google/kotlin/org/meshtastic/core/data/repository/CustomTileProviderRepository.kt +++ b/core/data/src/google/kotlin/org/meshtastic/core/data/repository/CustomTileProviderRepository.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.data.repository import co.touchlab.kermit.Logger @@ -26,7 +25,7 @@ import kotlinx.serialization.SerializationException import kotlinx.serialization.json.Json import org.meshtastic.core.data.model.CustomTileProviderConfig import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.map.MapTileProviderPrefs +import org.meshtastic.core.repository.MapTileProviderPrefs import javax.inject.Inject import javax.inject.Singleton @@ -82,7 +81,7 @@ constructor( customTileProvidersStateFlow.value.find { it.id == configId } private fun loadDataFromPrefs() { - val jsonString = mapTileProviderPrefs.customTileProviders + val jsonString = mapTileProviderPrefs.customTileProviders.value if (jsonString != null) { try { customTileProvidersStateFlow.value = json.decodeFromString>(jsonString) @@ -99,7 +98,7 @@ constructor( withContext(dispatchers.io) { try { val jsonString = json.encodeToString(providers) - mapTileProviderPrefs.customTileProviders = jsonString + mapTileProviderPrefs.setCustomTileProviders(jsonString) } catch (e: SerializationException) { Logger.e(e) { "Error serializing tile providers" } } diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DataStoreModule.kt similarity index 83% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/di/DataStoreModule.kt index 079be59b7..b34e2f52c 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/di/DataStoreModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DataStoreModule.kt @@ -14,12 +14,13 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -package org.meshtastic.core.datastore.di +package org.meshtastic.core.data.di import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler +import androidx.datastore.core.okio.OkioStorage import androidx.datastore.dataStoreFile import androidx.datastore.preferences.SharedPreferencesMigration import androidx.datastore.preferences.core.PreferenceDataStoreFactory @@ -34,6 +35,8 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import okio.FileSystem +import okio.Path.Companion.toOkioPath import org.meshtastic.core.datastore.KEY_APP_INTRO_COMPLETED import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN import org.meshtastic.core.datastore.KEY_NODE_SORT @@ -102,8 +105,12 @@ object DataStoreModule { @ApplicationContext appContext: Context, @DataStoreScope scope: CoroutineScope, ): DataStore = DataStoreFactory.create( - serializer = LocalConfigSerializer, - produceFile = { appContext.dataStoreFile("local_config.pb") }, + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = LocalConfigSerializer, + producePath = { appContext.dataStoreFile("local_config.pb").toOkioPath() }, + ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }), scope = scope, ) @@ -114,8 +121,12 @@ object DataStoreModule { @ApplicationContext appContext: Context, @DataStoreScope scope: CoroutineScope, ): DataStore = DataStoreFactory.create( - serializer = ModuleConfigSerializer, - produceFile = { appContext.dataStoreFile("module_config.pb") }, + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = ModuleConfigSerializer, + producePath = { appContext.dataStoreFile("module_config.pb").toOkioPath() }, + ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }), scope = scope, ) @@ -126,8 +137,12 @@ object DataStoreModule { @ApplicationContext appContext: Context, @DataStoreScope scope: CoroutineScope, ): DataStore = DataStoreFactory.create( - serializer = ChannelSetSerializer, - produceFile = { appContext.dataStoreFile("channel_set.pb") }, + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = ChannelSetSerializer, + producePath = { appContext.dataStoreFile("channel_set.pb").toOkioPath() }, + ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }), scope = scope, ) @@ -138,8 +153,12 @@ object DataStoreModule { @ApplicationContext appContext: Context, @DataStoreScope scope: CoroutineScope, ): DataStore = DataStoreFactory.create( - serializer = LocalStatsSerializer, - produceFile = { appContext.dataStoreFile("local_stats.pb") }, + storage = + OkioStorage( + fileSystem = FileSystem.SYSTEM, + serializer = LocalStatsSerializer, + producePath = { appContext.dataStoreFile("local_stats.pb").toOkioPath() }, + ), corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalStats() }), scope = scope, ) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt index c21be1920..6660fb87d 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/DatabaseModule.kt @@ -20,13 +20,15 @@ import dagger.Binds import dagger.Module import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import org.meshtastic.core.database.DatabaseManager import javax.inject.Singleton @InstallIn(SingletonComponent::class) @Module interface DatabaseModule { - @Binds @Singleton - fun bindDatabaseManager(impl: DatabaseManager): org.meshtastic.core.repository.DatabaseManager + @Binds + @Singleton + fun bindDatabaseManager( + impl: org.meshtastic.core.database.DatabaseManager, + ): org.meshtastic.core.common.database.DatabaseManager } diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt index 333398c10..5c48a3745 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/di/RepositoryModule.kt @@ -38,6 +38,7 @@ import org.meshtastic.core.data.manager.NodeManagerImpl import org.meshtastic.core.data.manager.PacketHandlerImpl import org.meshtastic.core.data.manager.TracerouteHandlerImpl import org.meshtastic.core.data.repository.DeviceHardwareRepositoryImpl +import org.meshtastic.core.data.repository.MeshLogRepositoryImpl import org.meshtastic.core.data.repository.NodeRepositoryImpl import org.meshtastic.core.data.repository.PacketRepositoryImpl import org.meshtastic.core.data.repository.RadioConfigRepositoryImpl @@ -51,6 +52,7 @@ import org.meshtastic.core.repository.MeshConfigFlowManager import org.meshtastic.core.repository.MeshConfigHandler import org.meshtastic.core.repository.MeshConnectionManager import org.meshtastic.core.repository.MeshDataHandler +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.MessageFilter @@ -85,6 +87,10 @@ abstract class RepositoryModule { @Binds @Singleton abstract fun bindPacketRepository(packetRepositoryImpl: PacketRepositoryImpl): PacketRepository + @Binds + @Singleton + abstract fun bindMeshLogRepository(meshLogRepositoryImpl: MeshLogRepositoryImpl): MeshLogRepository + @Binds @Singleton abstract fun bindNodeManager(nodeManagerImpl: NodeManagerImpl): NodeManager diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt index a2df3d73a..085966a2b 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/HistoryManagerImpl.kt @@ -18,8 +18,8 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger import okio.ByteString.Companion.toByteString -import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.HistoryManager +import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.PacketHandler import org.meshtastic.proto.Data import org.meshtastic.proto.MeshPacket @@ -71,7 +71,7 @@ constructor( } private fun activeDeviceAddress(): String? = - meshPrefs.deviceAddress?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() } + meshPrefs.deviceAddress.value?.takeIf { !it.equals(NO_DEVICE_SELECTED, ignoreCase = true) && it.isNotBlank() } override fun requestHistoryReplay( trigger: String, @@ -86,7 +86,7 @@ constructor( return } - val lastRequest = meshPrefs.getStoreForwardLastRequest(address) + val lastRequest = meshPrefs.getStoreForwardLastRequest(address).value val (window, max) = resolveHistoryRequestParameters( storeForwardConfig?.history_return_window ?: 0, @@ -116,7 +116,7 @@ constructor( override fun updateStoreForwardLastRequest(source: String, lastRequest: Int, transport: String) { if (lastRequest <= 0) return val address = activeDeviceAddress() ?: return - val current = meshPrefs.getStoreForwardLastRequest(address) + val current = meshPrefs.getStoreForwardLastRequest(address).value if (lastRequest != current) { meshPrefs.setStoreForwardLastRequest(address, lastRequest) historyLog( diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt index 15b3e8b90..07b30c0a7 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshActionHandlerImpl.kt @@ -23,6 +23,7 @@ import kotlinx.coroutines.SupervisorJob import okio.ByteString.Companion.toByteString import org.meshtastic.core.analytics.DataPair import org.meshtastic.core.analytics.platform.PlatformAnalytics +import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.ignoreException import org.meshtastic.core.common.util.nowMillis @@ -32,12 +33,11 @@ import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.Position import org.meshtastic.core.model.Reaction import org.meshtastic.core.model.service.ServiceAction -import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.repository.CommandSender -import org.meshtastic.core.repository.DatabaseManager import org.meshtastic.core.repository.MeshActionHandler import org.meshtastic.core.repository.MeshDataHandler import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshPrefs import org.meshtastic.core.repository.MeshServiceNotifications import org.meshtastic.core.repository.NodeManager import org.meshtastic.core.repository.PacketRepository @@ -197,7 +197,7 @@ constructor( override fun handleRequestPosition(destNum: Int, position: Position, myNodeNum: Int) { if (destNum != myNodeNum) { - val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum) + val provideLocation = meshPrefs.shouldProvideNodeLocation(myNodeNum).value val currentPosition = when { provideLocation && position.isValid() -> position @@ -343,9 +343,9 @@ constructor( } override fun handleUpdateLastAddress(deviceAddr: String?) { - val currentAddr = meshPrefs.deviceAddress + val currentAddr = meshPrefs.deviceAddress.value if (deviceAddr != currentAddr) { - meshPrefs.deviceAddress = deviceAddr + meshPrefs.setDeviceAddress(deviceAddr) scope.handledLaunch { nodeManager.clear() messageProcessor.get().clearEarlyPackets() diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index be2dd74c4..fbd87000c 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -35,7 +35,6 @@ import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.TelemetryType -import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager @@ -52,6 +51,7 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.connected import org.meshtastic.core.resources.connecting diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt index 1c19c8f31..cda802c89 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MeshMessageProcessorImpl.kt @@ -27,11 +27,11 @@ import kotlinx.coroutines.flow.onEach import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis import org.meshtastic.core.common.util.nowSeconds -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node import org.meshtastic.core.model.util.isLora import org.meshtastic.core.repository.FromRadioPacketHandler +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.MeshMessageProcessor import org.meshtastic.core.repository.MeshRouter import org.meshtastic.core.repository.NodeManager diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt index 906e615ae..a907c9a9f 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/MessageFilterImpl.kt @@ -17,7 +17,7 @@ package org.meshtastic.core.data.manager import co.touchlab.kermit.Logger -import org.meshtastic.core.prefs.filter.FilterPrefs +import org.meshtastic.core.repository.FilterPrefs import org.meshtastic.core.repository.MessageFilter import java.util.regex.PatternSyntaxException import javax.inject.Inject @@ -33,7 +33,7 @@ class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs } override fun shouldFilter(message: String, isFilteringDisabled: Boolean): Boolean { - if (!filterPrefs.filterEnabled || compiledPatterns.isEmpty() || isFilteringDisabled) { + if (!filterPrefs.filterEnabled.value || compiledPatterns.isEmpty() || isFilteringDisabled) { return false } val textToCheck = message.take(MAX_CHECK_LENGTH) @@ -42,7 +42,7 @@ class MessageFilterImpl @Inject constructor(private val filterPrefs: FilterPrefs override fun rebuildPatterns() { compiledPatterns = - filterPrefs.filterWords.mapNotNull { word -> + filterPrefs.filterWords.value.mapNotNull { word -> try { if (word.startsWith(REGEX_PREFIX)) { Regex(word.removePrefix(REGEX_PREFIX), RegexOption.IGNORE_CASE) diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt index a29cfed98..a42e77810 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/manager/PacketHandlerImpl.kt @@ -28,7 +28,6 @@ import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeoutOrNull import org.meshtastic.core.common.util.handledLaunch import org.meshtastic.core.common.util.nowMillis -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket @@ -36,6 +35,7 @@ import org.meshtastic.core.model.MessageStatus import org.meshtastic.core.model.RadioNotConnectedException import org.meshtastic.core.model.util.toOneLineString import org.meshtastic.core.model.util.toPIIString +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.PacketHandler import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioInterfaceService diff --git a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt similarity index 80% rename from core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt rename to core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt index 24a1cc825..7c09f1582 100644 --- a/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepository.kt +++ b/core/data/src/main/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryImpl.kt @@ -30,7 +30,9 @@ import org.meshtastic.core.data.datasource.NodeInfoReadDataSource import org.meshtastic.core.database.DatabaseManager import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.meshlog.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.core.repository.MeshLogRepository.Companion.DEFAULT_MAX_LOGS import org.meshtastic.proto.MeshPacket import org.meshtastic.proto.MyNodeInfo import org.meshtastic.proto.PortNum @@ -39,48 +41,48 @@ import javax.inject.Inject import javax.inject.Singleton /** - * Repository for managing and retrieving logs from the local database. + * Repository implementation for managing and retrieving logs from the local database. * * This repository provides methods for inserting, deleting, and querying logs, including specialized methods for * telemetry and traceroute data. */ @Suppress("TooManyFunctions") @Singleton -class MeshLogRepository +class MeshLogRepositoryImpl @Inject constructor( private val dbManager: DatabaseManager, private val dispatchers: CoroutineDispatchers, private val meshLogPrefs: MeshLogPrefs, private val nodeInfoReadDataSource: NodeInfoReadDataSource, -) { +) : MeshLogRepository { /** Retrieves all [MeshLog]s in the database, up to [maxItem]. */ - fun getAllLogs(maxItem: Int = MAX_MESH_PACKETS): Flow> = + override fun getAllLogs(maxItem: Int): Flow> = dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogs(maxItem) }.flowOn(dispatchers.io) /** Retrieves all [MeshLog]s in the database in the order they were received. */ - fun getAllLogsInReceiveOrder(maxItem: Int = MAX_MESH_PACKETS): Flow> = + override fun getAllLogsInReceiveOrder(maxItem: Int): Flow> = dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItem) }.flowOn(dispatchers.io) /** Retrieves all [MeshLog]s in the database without any limit. */ - fun getAllLogsUnbounded(): Flow> = getAllLogs(Int.MAX_VALUE) + override fun getAllLogsUnbounded(): Flow> = getAllLogs(Int.MAX_VALUE) /** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */ - fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> = dbManager.currentDb - .flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, MAX_MESH_PACKETS) } + override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> = dbManager.currentDb + .flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, DEFAULT_MAX_LOGS) } .distinctUntilChanged() .flowOn(dispatchers.io) /** Retrieves all [MeshLog]s containing [MeshPacket]s for a specific [nodeNum]. */ - fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = -1): Flow> = + override fun getMeshPacketsFrom(nodeNum: Int, portNum: Int): Flow> = getLogsFrom(nodeNum, portNum).map { list -> list.mapNotNull { it.fromRadio.packet } }.flowOn(dispatchers.io) /** Retrieves telemetry history for a specific node, automatically handling local node redirection. */ - fun getTelemetryFrom(nodeNum: Int): Flow> = effectiveLogId(nodeNum) + override fun getTelemetryFrom(nodeNum: Int): Flow> = effectiveLogId(nodeNum) .flatMapLatest { logId -> dbManager.currentDb - .flatMapLatest { it.meshLogDao().getLogsFrom(logId, PortNum.TELEMETRY_APP.value, MAX_MESH_PACKETS) } + .flatMapLatest { it.meshLogDao().getLogsFrom(logId, PortNum.TELEMETRY_APP.value, DEFAULT_MAX_LOGS) } .distinctUntilChanged() .mapLatest { list -> list.mapNotNull(::parseTelemetryLog) } } @@ -91,8 +93,8 @@ constructor( * * A request log is defined as an outgoing packet (`fromNum = 0`) where `want_response` is true. */ - fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow> = dbManager.currentDb - .flatMapLatest { it.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, portNum.value, MAX_MESH_PACKETS) } + override fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow> = dbManager.currentDb + .flatMapLatest { it.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, portNum.value, DEFAULT_MAX_LOGS) } .map { list -> list.filter { log -> val packet = log.fromRadio.packet ?: return@filter false @@ -140,26 +142,27 @@ constructor( .distinctUntilChanged() /** Returns the cached [MyNodeInfo] from the system logs. */ - fun getMyNodeInfo(): Flow = dbManager.currentDb - .flatMapLatest { db -> db.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, 0, MAX_MESH_PACKETS) } + override fun getMyNodeInfo(): Flow = dbManager.currentDb + .flatMapLatest { db -> db.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, 0, DEFAULT_MAX_LOGS) } .mapLatest { list -> list.firstOrNull { it.myNodeInfo != null }?.myNodeInfo } .flowOn(dispatchers.io) /** Persists a new log entry to the database if logging is enabled in preferences. */ - suspend fun insert(log: MeshLog) = withContext(dispatchers.io) { - if (!meshLogPrefs.loggingEnabled) return@withContext + override suspend fun insert(log: MeshLog) = withContext(dispatchers.io) { + if (!meshLogPrefs.loggingEnabled.value) return@withContext dbManager.currentDb.value.meshLogDao().insert(log) } /** Clears all logs from the database. */ - suspend fun deleteAll() = withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteAll() } + override suspend fun deleteAll() = + withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteAll() } /** Deletes a specific log entry by its [uuid]. */ - suspend fun deleteLog(uuid: String) = + override suspend fun deleteLog(uuid: String) = withContext(dispatchers.io) { dbManager.currentDb.value.meshLogDao().deleteLog(uuid) } /** Deletes all logs associated with a specific [nodeNum] and [portNum]. */ - suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(dispatchers.io) { + override suspend fun deleteLogs(nodeNum: Int, portNum: Int) = withContext(dispatchers.io) { val myNodeNum = nodeInfoReadDataSource.myNodeInfoFlow().firstOrNull()?.myNodeNum val logId = if (nodeNum == myNodeNum) MeshLog.NODE_NUM_LOCAL else nodeNum dbManager.currentDb.value.meshLogDao().deleteLogs(logId, portNum) @@ -167,13 +170,12 @@ constructor( /** Prunes the log database based on the configured [retentionDays]. */ @Suppress("MagicNumber") - suspend fun deleteLogsOlderThan(retentionDays: Int) = withContext(dispatchers.io) { + override suspend fun deleteLogsOlderThan(retentionDays: Int) = withContext(dispatchers.io) { val cutoffTime = nowMillis - (retentionDays.toLong() * 24 * 60 * 60 * 1000) dbManager.currentDb.value.meshLogDao().deleteOlderThan(cutoffTime) } companion object { - private const val MAX_MESH_PACKETS = 5000 private const val MILLIS_PER_SEC = 1000L } } diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index c21b43c69..258756e9c 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -36,7 +36,6 @@ import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DataPacket import org.meshtastic.core.model.MyNodeInfo import org.meshtastic.core.model.Node -import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.repository.AppWidgetUpdater import org.meshtastic.core.repository.CommandSender import org.meshtastic.core.repository.HistoryManager @@ -52,6 +51,7 @@ import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.UiPrefs import org.meshtastic.core.resources.getString import org.meshtastic.proto.Config import org.meshtastic.proto.LocalConfig diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt index 65c77ec7e..d7e7c565d 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/MessageFilterImplTest.kt @@ -18,34 +18,39 @@ package org.meshtastic.core.data.manager import io.mockk.every import io.mockk.mockk +import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test -import org.meshtastic.core.prefs.filter.FilterPrefs +import org.meshtastic.core.repository.FilterPrefs class MessageFilterImplTest { private lateinit var filterPrefs: FilterPrefs + private lateinit var filterEnabledFlow: MutableStateFlow + private lateinit var filterWordsFlow: MutableStateFlow> private lateinit var filterService: MessageFilterImpl @Before fun setup() { + filterEnabledFlow = MutableStateFlow(true) + filterWordsFlow = MutableStateFlow(setOf("spam", "bad")) filterPrefs = mockk { - every { filterEnabled } returns true - every { filterWords } returns setOf("spam", "bad") + every { filterEnabled } returns filterEnabledFlow + every { filterWords } returns filterWordsFlow } filterService = MessageFilterImpl(filterPrefs) } @Test fun `shouldFilter returns false when filter is disabled`() { - every { filterPrefs.filterEnabled } returns false + filterEnabledFlow.value = false assertFalse(filterService.shouldFilter("spam message")) } @Test fun `shouldFilter returns false when filter words is empty`() { - every { filterPrefs.filterWords } returns emptySet() + filterWordsFlow.value = emptySet() filterService.rebuildPatterns() assertFalse(filterService.shouldFilter("any message")) } @@ -70,7 +75,7 @@ class MessageFilterImplTest { @Test fun `shouldFilter supports regex patterns`() { - every { filterPrefs.filterWords } returns setOf("regex:test\\d+") + filterWordsFlow.value = setOf("regex:test\\d+") filterService.rebuildPatterns() assertTrue(filterService.shouldFilter("this is test123")) assertFalse(filterService.shouldFilter("this is test")) @@ -78,7 +83,7 @@ class MessageFilterImplTest { @Test fun `shouldFilter handles invalid regex gracefully`() { - every { filterPrefs.filterWords } returns setOf("regex:[invalid") + filterWordsFlow.value = setOf("regex:[invalid") filterService.rebuildPatterns() assertFalse(filterService.shouldFilter("any message")) } diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt index 4447ec440..2486922ac 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/manager/PacketHandlerImplTest.kt @@ -26,9 +26,9 @@ import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts diff --git a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt index 78c56d8c1..06afd655e 100644 --- a/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt +++ b/core/data/src/test/kotlin/org/meshtastic/core/data/repository/MeshLogRepositoryTest.kt @@ -36,7 +36,7 @@ import org.meshtastic.core.database.dao.MeshLogDao import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.database.entity.MyNodeEntity import org.meshtastic.core.di.CoroutineDispatchers -import org.meshtastic.core.prefs.meshlog.MeshLogPrefs +import org.meshtastic.core.repository.MeshLogPrefs import org.meshtastic.proto.Data import org.meshtastic.proto.EnvironmentMetrics import org.meshtastic.proto.FromRadio @@ -55,7 +55,7 @@ class MeshLogRepositoryTest { private val testDispatcher = UnconfinedTestDispatcher() private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher) - private val repository = MeshLogRepository(dbManager, dispatchers, meshLogPrefs, nodeInfoReadDataSource) + private val repository = MeshLogRepositoryImpl(dbManager, dispatchers, meshLogPrefs, nodeInfoReadDataSource) init { every { dbManager.currentDb } returns MutableStateFlow(appDatabase) @@ -81,7 +81,7 @@ class MeshLogRepositoryTest { ) // Using reflection to test private method parseTelemetryLog - val method = MeshLogRepository::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java) + val method = MeshLogRepositoryImpl::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java) method.isAccessible = true val result = method.invoke(repository, meshLog) as Telemetry? @@ -107,7 +107,7 @@ class MeshLogRepositoryTest { fromRadio = FromRadio(packet = meshPacket), ) - val method = MeshLogRepository::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java) + val method = MeshLogRepositoryImpl::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java) method.isAccessible = true val result = method.invoke(repository, meshLog) as Telemetry? diff --git a/core/database/build.gradle.kts b/core/database/build.gradle.kts index e97a8d3ed..026a9b410 100644 --- a/core/database/build.gradle.kts +++ b/core/database/build.gradle.kts @@ -32,7 +32,7 @@ kotlin { sourceSets { commonMain.dependencies { implementation(libs.androidx.sqlite.bundled) - implementation(projects.core.repository) + api(projects.core.common) implementation(projects.core.di) api(projects.core.model) diff --git a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 1a6181f92..e5c96cd41 100644 --- a/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/androidMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -40,7 +40,7 @@ import org.meshtastic.core.di.CoroutineDispatchers import java.io.File import javax.inject.Inject import javax.inject.Singleton -import org.meshtastic.core.repository.DatabaseManager as SharedDatabaseManager +import org.meshtastic.core.common.database.DatabaseManager as SharedDatabaseManager /** Manages per-device Room database instances for node data, with LRU eviction. */ @Singleton diff --git a/core/datastore/build.gradle.kts b/core/datastore/build.gradle.kts index 40c3a389d..874153009 100644 --- a/core/datastore/build.gradle.kts +++ b/core/datastore/build.gradle.kts @@ -14,38 +14,29 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -import com.android.build.api.dsl.LibraryExtension - -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - plugins { - alias(libs.plugins.meshtastic.android.library) - alias(libs.plugins.meshtastic.hilt) + alias(libs.plugins.meshtastic.kmp.library) alias(libs.plugins.meshtastic.kotlinx.serialization) + alias(libs.plugins.kotlin.parcelize) + alias(libs.plugins.devtools.ksp) } -configure { namespace = "org.meshtastic.core.datastore" } +kotlin { + android { namespace = "org.meshtastic.core.datastore" } -dependencies { - implementation(projects.core.proto) - - implementation(libs.androidx.datastore) - implementation(libs.androidx.datastore.preferences) - implementation(libs.kotlinx.serialization.json) - implementation(libs.kermit) + sourceSets { + commonMain.dependencies { + implementation(projects.core.proto) + implementation(libs.androidx.datastore) + implementation(libs.androidx.datastore.preferences) + implementation(libs.kotlinx.serialization.json) + implementation(libs.kermit) + } + androidMain.dependencies { + implementation(libs.hilt.android) + implementation(libs.javax.inject) + } + } } + +dependencies { "kspAndroid"(libs.hilt.compiler) } diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt similarity index 98% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt index f90176671..5eda0ca4c 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/BootloaderWarningDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.datastore import androidx.datastore.core.DataStore diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt similarity index 100% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ChannelSetDataSource.kt diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt similarity index 100% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalConfigDataSource.kt diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt similarity index 100% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/LocalStatsDataSource.kt diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt similarity index 100% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/ModuleConfigDataSource.kt diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt similarity index 99% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt index 63501dc91..0d3c4c123 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/RecentAddressesDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.datastore import androidx.datastore.core.DataStore diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt similarity index 90% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt index 69a49a521..02634293e 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/UiPreferencesDataSource.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.datastore import androidx.datastore.core.DataStore @@ -33,16 +32,16 @@ import kotlinx.coroutines.launch import javax.inject.Inject import javax.inject.Singleton -internal const val KEY_APP_INTRO_COMPLETED = "app_intro_completed" -internal const val KEY_THEME = "theme" +const val KEY_APP_INTRO_COMPLETED = "app_intro_completed" +const val KEY_THEME = "theme" // Node list filters/sort -internal const val KEY_NODE_SORT = "node-sort-option" -internal const val KEY_INCLUDE_UNKNOWN = "include-unknown" -internal const val KEY_EXCLUDE_INFRASTRUCTURE = "exclude-infrastructure" -internal const val KEY_ONLY_ONLINE = "only-online" -internal const val KEY_ONLY_DIRECT = "only-direct" -internal const val KEY_SHOW_IGNORED = "show-ignored" +const val KEY_NODE_SORT = "node-sort-option" +const val KEY_INCLUDE_UNKNOWN = "include-unknown" +const val KEY_EXCLUDE_INFRASTRUCTURE = "exclude-infrastructure" +const val KEY_ONLY_ONLINE = "only-online" +const val KEY_ONLY_DIRECT = "only-direct" +const val KEY_SHOW_IGNORED = "show-ignored" @Singleton class UiPreferencesDataSource @Inject constructor(private val dataStore: DataStore) { diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt similarity index 95% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt index 4cbb90320..f3a087f04 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/model/RecentAddress.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.datastore.model import kotlinx.serialization.Serializable diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt similarity index 72% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt index 800b099f2..a46b2f4f7 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ChannelSetSerializer.kt @@ -17,24 +17,25 @@ package org.meshtastic.core.datastore.serializer import androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer +import androidx.datastore.core.okio.OkioSerializer +import okio.BufferedSink +import okio.BufferedSource import okio.IOException import org.meshtastic.proto.ChannelSet -import java.io.InputStream -import java.io.OutputStream /** Serializer for the [ChannelSet] object defined in apponly.proto. */ -@Suppress("BlockingMethodInNonBlockingContext") -object ChannelSetSerializer : Serializer { +object ChannelSetSerializer : OkioSerializer { override val defaultValue: ChannelSet = ChannelSet() - override suspend fun readFrom(input: InputStream): ChannelSet { + override suspend fun readFrom(source: BufferedSource): ChannelSet { try { - return ChannelSet.ADAPTER.decode(input) + return ChannelSet.ADAPTER.decode(source) } catch (exception: IOException) { throw CorruptionException("Cannot read proto.", exception) } } - override suspend fun writeTo(t: ChannelSet, output: OutputStream) = ChannelSet.ADAPTER.encode(output, t) + override suspend fun writeTo(t: ChannelSet, sink: BufferedSink) { + ChannelSet.ADAPTER.encode(sink, t) + } } diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt similarity index 72% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt index f356aa158..14988d461 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalConfigSerializer.kt @@ -17,24 +17,25 @@ package org.meshtastic.core.datastore.serializer import androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer +import androidx.datastore.core.okio.OkioSerializer +import okio.BufferedSink +import okio.BufferedSource import okio.IOException import org.meshtastic.proto.LocalConfig -import java.io.InputStream -import java.io.OutputStream /** Serializer for the [LocalConfig] object defined in localonly.proto. */ -@Suppress("BlockingMethodInNonBlockingContext") -object LocalConfigSerializer : Serializer { +object LocalConfigSerializer : OkioSerializer { override val defaultValue: LocalConfig = LocalConfig() - override suspend fun readFrom(input: InputStream): LocalConfig { + override suspend fun readFrom(source: BufferedSource): LocalConfig { try { - return LocalConfig.ADAPTER.decode(input) + return LocalConfig.ADAPTER.decode(source) } catch (exception: IOException) { throw CorruptionException("Cannot read proto.", exception) } } - override suspend fun writeTo(t: LocalConfig, output: OutputStream) = LocalConfig.ADAPTER.encode(output, t) + override suspend fun writeTo(t: LocalConfig, sink: BufferedSink) { + LocalConfig.ADAPTER.encode(sink, t) + } } diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt similarity index 72% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt index 8f1e2d68f..83b9f5481 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/LocalStatsSerializer.kt @@ -17,24 +17,25 @@ package org.meshtastic.core.datastore.serializer import androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer +import androidx.datastore.core.okio.OkioSerializer +import okio.BufferedSink +import okio.BufferedSource import okio.IOException import org.meshtastic.proto.LocalStats -import java.io.InputStream -import java.io.OutputStream /** Serializer for the [LocalStats] object defined in telemetry.proto. */ -@Suppress("BlockingMethodInNonBlockingContext") -object LocalStatsSerializer : Serializer { +object LocalStatsSerializer : OkioSerializer { override val defaultValue: LocalStats = LocalStats() - override suspend fun readFrom(input: InputStream): LocalStats { + override suspend fun readFrom(source: BufferedSource): LocalStats { try { - return LocalStats.ADAPTER.decode(input) + return LocalStats.ADAPTER.decode(source) } catch (exception: IOException) { throw CorruptionException("Cannot read proto.", exception) } } - override suspend fun writeTo(t: LocalStats, output: OutputStream) = LocalStats.ADAPTER.encode(output, t) + override suspend fun writeTo(t: LocalStats, sink: BufferedSink) { + LocalStats.ADAPTER.encode(sink, t) + } } diff --git a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt similarity index 71% rename from core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt rename to core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt index 14087b4fd..419ca6970 100644 --- a/core/datastore/src/main/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt +++ b/core/datastore/src/commonMain/kotlin/org/meshtastic/core/datastore/serializer/ModuleConfigSerializer.kt @@ -17,25 +17,25 @@ package org.meshtastic.core.datastore.serializer import androidx.datastore.core.CorruptionException -import androidx.datastore.core.Serializer +import androidx.datastore.core.okio.OkioSerializer +import okio.BufferedSink +import okio.BufferedSource import okio.IOException import org.meshtastic.proto.LocalModuleConfig -import java.io.InputStream -import java.io.OutputStream /** Serializer for the [LocalModuleConfig] object defined in localonly.proto. */ -@Suppress("BlockingMethodInNonBlockingContext") -object ModuleConfigSerializer : Serializer { +object ModuleConfigSerializer : OkioSerializer { override val defaultValue: LocalModuleConfig = LocalModuleConfig() - override suspend fun readFrom(input: InputStream): LocalModuleConfig { + override suspend fun readFrom(source: BufferedSource): LocalModuleConfig { try { - return LocalModuleConfig.ADAPTER.decode(input) + return LocalModuleConfig.ADAPTER.decode(source) } catch (exception: IOException) { throw CorruptionException("Cannot read proto.", exception) } } - override suspend fun writeTo(t: LocalModuleConfig, output: OutputStream) = - LocalModuleConfig.ADAPTER.encode(output, t) + override suspend fun writeTo(t: LocalModuleConfig, sink: BufferedSink) { + LocalModuleConfig.ADAPTER.encode(sink, t) + } } diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index c368cd45d..d78eb1c6c 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -29,8 +29,6 @@ dependencies { implementation(projects.core.proto) implementation(projects.core.common) implementation(projects.core.database) - implementation(projects.core.prefs) - implementation(projects.core.data) implementation(projects.core.datastore) implementation(projects.core.resources) diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt index aea9301d4..ce7261863 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCase.kt @@ -18,9 +18,9 @@ package org.meshtastic.core.domain.usecase.settings import android.icu.text.SimpleDateFormat import kotlinx.coroutines.flow.first -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.model.Position import org.meshtastic.core.model.util.positionToMeter +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.PortNum import java.io.BufferedWriter diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt index f77a09345..1707a7500 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCase.kt @@ -23,12 +23,12 @@ import kotlinx.coroutines.flow.flowOf import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node 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 javax.inject.Inject /** Use case to determine if the currently connected device is capable of over-the-air (OTA) updates. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt index 42224e849..4b46cd70c 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCase.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.core.domain.usecase.settings +import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.database.DatabaseConstants -import org.meshtastic.core.repository.DatabaseManager import javax.inject.Inject /** Use case for setting the database cache limit. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt index cdb822dde..b18133635 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCase.kt @@ -16,8 +16,8 @@ */ package org.meshtastic.core.domain.usecase.settings -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 javax.inject.Inject /** Use case for managing mesh log settings. */ @@ -34,7 +34,7 @@ constructor( */ suspend fun setRetentionDays(days: Int) { val clamped = days.coerceIn(MeshLogPrefs.MIN_RETENTION_DAYS, MeshLogPrefs.MAX_RETENTION_DAYS) - meshLogPrefs.retentionDays = clamped + meshLogPrefs.setRetentionDays(clamped) meshLogRepository.deleteLogsOlderThan(clamped) } @@ -44,11 +44,11 @@ constructor( * @param enabled True to enable logging, false to disable. */ suspend fun setLoggingEnabled(enabled: Boolean) { - meshLogPrefs.loggingEnabled = enabled + meshLogPrefs.setLoggingEnabled(enabled) if (!enabled) { meshLogRepository.deleteAll() } else { - meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays) + meshLogRepository.deleteLogsOlderThan(meshLogPrefs.retentionDays.value) } } } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt index 3a45c3e43..e66651f9c 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCase.kt @@ -16,7 +16,7 @@ */ package org.meshtastic.core.domain.usecase.settings -import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.UiPrefs import javax.inject.Inject /** Use case for setting whether to provide the node location to the mesh. */ diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt index b8e6f2d29..92aa6933c 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCase.kt @@ -16,12 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings -import org.meshtastic.core.prefs.analytics.AnalyticsPrefs +import org.meshtastic.core.repository.AnalyticsPrefs import javax.inject.Inject /** Use case for toggling the analytics preference. */ open class ToggleAnalyticsUseCase @Inject constructor(private val analyticsPrefs: AnalyticsPrefs) { operator fun invoke() { - analyticsPrefs.analyticsAllowed = !analyticsPrefs.analyticsAllowed + analyticsPrefs.setAnalyticsAllowed(!analyticsPrefs.analyticsAllowed.value) } } diff --git a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt index f42dee80b..37d693e1f 100644 --- a/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt +++ b/core/domain/src/main/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCase.kt @@ -16,12 +16,12 @@ */ package org.meshtastic.core.domain.usecase.settings -import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs +import org.meshtastic.core.repository.HomoglyphPrefs import javax.inject.Inject /** Use case for toggling the homoglyph encoding preference. */ open class ToggleHomoglyphEncodingUseCase @Inject constructor(private val homoglyphEncodingPrefs: HomoglyphPrefs) { operator fun invoke() { - homoglyphEncodingPrefs.homoglyphEncodingEnabled = !homoglyphEncodingPrefs.homoglyphEncodingEnabled + homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(!homoglyphEncodingPrefs.homoglyphEncodingEnabled.value) } } diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt index fac5b04e4..c10045b88 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/SendMessageUseCaseTest.kt @@ -81,7 +81,7 @@ class SendMessageUseCaseTest { val ourNode = mockk(relaxed = true) every { ourNode.user.id } returns "!1234" every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode) - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns false // Act useCase("Hello broadcast", "0${DataPacket.ID_BROADCAST}", null) @@ -110,7 +110,7 @@ class SendMessageUseCaseTest { every { destNode.num } returns 12345 every { nodeRepository.getNode("!dest") } returns destNode - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns false every { anyConstructed().canSendVerifiedContacts } returns false // Act @@ -139,7 +139,7 @@ class SendMessageUseCaseTest { every { destNode.num } returns 67890 every { nodeRepository.getNode("!dest") } returns destNode - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns false every { anyConstructed().canSendVerifiedContacts } returns true // Act @@ -158,7 +158,7 @@ class SendMessageUseCaseTest { // Arrange val ourNode = mockk(relaxed = true) every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode) - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns true val originalText = "\u0410pple" // Cyrillic A diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt index 5e3a05cab..f97ffe525 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ExportDataUseCaseTest.kt @@ -26,9 +26,9 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.data.repository.MeshLogRepository import org.meshtastic.core.database.entity.MeshLog import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.MeshLogRepository import org.meshtastic.core.repository.NodeRepository import org.meshtastic.proto.Data import org.meshtastic.proto.FromRadio diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt index 8e6b21077..dc17b7cd2 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/IsOtaCapableUseCaseTest.kt @@ -29,9 +29,9 @@ import org.junit.Test import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.Node import org.meshtastic.core.model.RadioController -import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.repository.DeviceHardwareRepository import org.meshtastic.core.repository.NodeRepository +import org.meshtastic.core.repository.RadioPrefs class IsOtaCapableUseCaseTest { @@ -82,7 +82,7 @@ class IsOtaCapableUseCaseTest { val node = mockk(relaxed = true) ourNodeInfoFlow.value = node connectionStateFlow.value = ConnectionState.Connected - every { radioPrefs.devAddr } returns "m123" // Mock + every { radioPrefs.devAddr } returns MutableStateFlow("m123") // Mock useCase().test { assertFalse(awaitItem()) @@ -95,7 +95,7 @@ class IsOtaCapableUseCaseTest { val node = mockk(relaxed = true) ourNodeInfoFlow.value = node connectionStateFlow.value = ConnectionState.Connected - every { radioPrefs.devAddr } returns "x123" // BLE + every { radioPrefs.devAddr } returns MutableStateFlow("x123") // BLE val hw = mockk { every { requiresDfu } returns true } coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw) @@ -111,7 +111,7 @@ class IsOtaCapableUseCaseTest { val node = mockk(relaxed = true) ourNodeInfoFlow.value = node connectionStateFlow.value = ConnectionState.Connected - every { radioPrefs.devAddr } returns "x123" // BLE + every { radioPrefs.devAddr } returns MutableStateFlow("x123") // BLE val hw = mockk { every { requiresDfu } returns false } coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw) diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt index 78a22de2f..8a31155ad 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetDatabaseCacheLimitUseCaseTest.kt @@ -20,8 +20,8 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test +import org.meshtastic.core.common.database.DatabaseManager import org.meshtastic.core.database.DatabaseConstants -import org.meshtastic.core.repository.DatabaseManager class SetDatabaseCacheLimitUseCaseTest { diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt index 748587b6a..cac857b69 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetMeshLogSettingsUseCaseTest.kt @@ -23,8 +23,8 @@ import io.mockk.verify import kotlinx.coroutines.test.runTest 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 class SetMeshLogSettingsUseCaseTest { @@ -45,20 +45,20 @@ class SetMeshLogSettingsUseCaseTest { useCase.setRetentionDays(MeshLogPrefs.MIN_RETENTION_DAYS - 1) // Assert - verify { meshLogPrefs.retentionDays = MeshLogPrefs.MIN_RETENTION_DAYS } + verify { meshLogPrefs.setRetentionDays(MeshLogPrefs.MIN_RETENTION_DAYS) } coVerify { meshLogRepository.deleteLogsOlderThan(MeshLogPrefs.MIN_RETENTION_DAYS) } } @Test fun `setLoggingEnabled true triggers cleanup`() = runTest { // Arrange - every { meshLogPrefs.retentionDays } returns 30 + every { meshLogPrefs.retentionDays.value } returns 30 // Act useCase.setLoggingEnabled(true) // Assert - verify { meshLogPrefs.loggingEnabled = true } + verify { meshLogPrefs.setLoggingEnabled(true) } coVerify { meshLogRepository.deleteLogsOlderThan(30) } } @@ -68,7 +68,7 @@ class SetMeshLogSettingsUseCaseTest { useCase.setLoggingEnabled(false) // Assert - verify { meshLogPrefs.loggingEnabled = false } + verify { meshLogPrefs.setLoggingEnabled(false) } coVerify { meshLogRepository.deleteAll() } } } diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCaseTest.kt index 240b07876..5877cbf1e 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/SetProvideLocationUseCaseTest.kt @@ -20,7 +20,7 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test -import org.meshtastic.core.prefs.ui.UiPrefs +import org.meshtastic.core.repository.UiPrefs class SetProvideLocationUseCaseTest { diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCaseTest.kt index 63fbf2b2a..3dea1fd20 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleAnalyticsUseCaseTest.kt @@ -21,7 +21,7 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test -import org.meshtastic.core.prefs.analytics.AnalyticsPrefs +import org.meshtastic.core.repository.AnalyticsPrefs class ToggleAnalyticsUseCaseTest { @@ -37,24 +37,24 @@ class ToggleAnalyticsUseCaseTest { @Test fun `invoke toggles analytics from false to true`() { // Arrange - every { analyticsPrefs.analyticsAllowed } returns false + every { analyticsPrefs.analyticsAllowed.value } returns false // Act useCase() // Assert - verify { analyticsPrefs.analyticsAllowed = true } + verify { analyticsPrefs.setAnalyticsAllowed(true) } } @Test fun `invoke toggles analytics from true to false`() { // Arrange - every { analyticsPrefs.analyticsAllowed } returns true + every { analyticsPrefs.analyticsAllowed.value } returns true // Act useCase() // Assert - verify { analyticsPrefs.analyticsAllowed = false } + verify { analyticsPrefs.setAnalyticsAllowed(false) } } } diff --git a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCaseTest.kt b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCaseTest.kt index f8cf978af..9789ad703 100644 --- a/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCaseTest.kt +++ b/core/domain/src/test/kotlin/org/meshtastic/core/domain/usecase/settings/ToggleHomoglyphEncodingUseCaseTest.kt @@ -21,7 +21,7 @@ import io.mockk.mockk import io.mockk.verify import org.junit.Before import org.junit.Test -import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs +import org.meshtastic.core.repository.HomoglyphPrefs class ToggleHomoglyphEncodingUseCaseTest { @@ -37,24 +37,24 @@ class ToggleHomoglyphEncodingUseCaseTest { @Test fun `invoke toggles homoglyph encoding from false to true`() { // Arrange - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns false + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns false // Act useCase() // Assert - verify { homoglyphEncodingPrefs.homoglyphEncodingEnabled = true } + verify { homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(true) } } @Test fun `invoke toggles homoglyph encoding from true to false`() { // Arrange - every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true + every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns true // Act useCase() // Assert - verify { homoglyphEncodingPrefs.homoglyphEncodingEnabled = false } + verify { homoglyphEncodingPrefs.setHomoglyphEncodingEnabled(false) } } } diff --git a/core/prefs/build.gradle.kts b/core/prefs/build.gradle.kts index 227428272..844495e6b 100644 --- a/core/prefs/build.gradle.kts +++ b/core/prefs/build.gradle.kts @@ -26,8 +26,13 @@ configure { namespace = "org.meshtastic.core.prefs" } dependencies { implementation(projects.core.repository) + implementation(projects.core.common) + implementation(projects.core.di) + implementation(libs.androidx.datastore.preferences) + implementation(libs.kotlinx.coroutines.core) googleImplementation(libs.maps.compose) testImplementation(libs.junit) testImplementation(libs.mockk) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/di/GoogleMapsModule.kt b/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/di/GoogleMapsModule.kt index 79d0eb3ff..d195087f7 100644 --- a/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/di/GoogleMapsModule.kt +++ b/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/di/GoogleMapsModule.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,28 +14,31 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.prefs.di import android.content.Context -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.prefs.map.GoogleMapsPrefs import org.meshtastic.core.prefs.map.GoogleMapsPrefsImpl import javax.inject.Qualifier import javax.inject.Singleton -// Pref store qualifiers are internal to prevent prefs stores from being injected directly. -// Consuming code should always inject one of the prefs repositories. - @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class GoogleMapsSharedPreferences +internal annotation class GoogleMapsDataStore @InstallIn(SingletonComponent::class) @Module @@ -44,11 +47,16 @@ interface GoogleMapsModule { @Binds fun bindGoogleMapsPrefs(googleMapsPrefsImpl: GoogleMapsPrefsImpl): GoogleMapsPrefs companion object { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @Provides @Singleton - @GoogleMapsSharedPreferences - fun provideGoogleMapsSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("google_maps_prefs", Context.MODE_PRIVATE) + @GoogleMapsDataStore + fun provideGoogleMapsDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("google_maps_ds") }, + ) } } diff --git a/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/map/GoogleMapsPrefs.kt b/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/map/GoogleMapsPrefs.kt index 73942c308..a8873201d 100644 --- a/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/map/GoogleMapsPrefs.kt +++ b/core/prefs/src/google/kotlin/org/meshtastic/core/prefs/map/GoogleMapsPrefs.kt @@ -16,39 +16,168 @@ */ package org.meshtastic.core.prefs.map -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.doublePreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.floatPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.core.stringSetPreferencesKey import com.google.maps.android.compose.MapType -import org.meshtastic.core.prefs.DoublePrefDelegate -import org.meshtastic.core.prefs.FloatPrefDelegate -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.StringSetPrefDelegate -import org.meshtastic.core.prefs.di.GoogleMapsSharedPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.GoogleMapsDataStore import javax.inject.Inject import javax.inject.Singleton /** Interface for prefs specific to Google Maps. For general map prefs, see MapPrefs. */ interface GoogleMapsPrefs { - var selectedGoogleMapType: String? - var selectedCustomTileUrl: String? - var hiddenLayerUrls: Set - var cameraTargetLat: Double - var cameraTargetLng: Double - var cameraZoom: Float - var cameraTilt: Float - var cameraBearing: Float - var networkMapLayers: Set + val selectedGoogleMapType: StateFlow + + fun setSelectedGoogleMapType(value: String?) + + val selectedCustomTileUrl: StateFlow + + fun setSelectedCustomTileUrl(value: String?) + + val hiddenLayerUrls: StateFlow> + + fun setHiddenLayerUrls(value: Set) + + val cameraTargetLat: StateFlow + + fun setCameraTargetLat(value: Double) + + val cameraTargetLng: StateFlow + + fun setCameraTargetLng(value: Double) + + val cameraZoom: StateFlow + + fun setCameraZoom(value: Float) + + val cameraTilt: StateFlow + + fun setCameraTilt(value: Float) + + val cameraBearing: StateFlow + + fun setCameraBearing(value: Float) + + val networkMapLayers: StateFlow> + + fun setNetworkMapLayers(value: Set) } @Singleton -class GoogleMapsPrefsImpl @Inject constructor(@GoogleMapsSharedPreferences prefs: SharedPreferences) : GoogleMapsPrefs { - override var selectedGoogleMapType: String? by - NullableStringPrefDelegate(prefs, "selected_google_map_type", MapType.NORMAL.name) - override var selectedCustomTileUrl: String? by NullableStringPrefDelegate(prefs, "selected_custom_tile_url", null) - override var hiddenLayerUrls: Set by StringSetPrefDelegate(prefs, "hidden_layer_urls", emptySet()) - override var cameraTargetLat: Double by DoublePrefDelegate(prefs, "camera_target_lat", 0.0) - override var cameraTargetLng: Double by DoublePrefDelegate(prefs, "camera_target_lng", 0.0) - override var cameraZoom: Float by FloatPrefDelegate(prefs, "camera_zoom", 7f) - override var cameraTilt: Float by FloatPrefDelegate(prefs, "camera_tilt", 0f) - override var cameraBearing: Float by FloatPrefDelegate(prefs, "camera_bearing", 0f) - override var networkMapLayers: Set by StringSetPrefDelegate(prefs, "network_map_layers", emptySet()) +class GoogleMapsPrefsImpl +@Inject +constructor( + @GoogleMapsDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : GoogleMapsPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val selectedGoogleMapType: StateFlow = + dataStore.data + .map { it[KEY_SELECTED_GOOGLE_MAP_TYPE_PREF] ?: MapType.NORMAL.name } + .stateIn(scope, SharingStarted.Eagerly, MapType.NORMAL.name) + + override fun setSelectedGoogleMapType(value: String?) { + scope.launch { + dataStore.edit { prefs -> + if (value == null) { + prefs.remove(KEY_SELECTED_GOOGLE_MAP_TYPE_PREF) + } else { + prefs[KEY_SELECTED_GOOGLE_MAP_TYPE_PREF] = value + } + } + } + } + + override val selectedCustomTileUrl: StateFlow = + dataStore.data.map { it[KEY_SELECTED_CUSTOM_TILE_URL_PREF] }.stateIn(scope, SharingStarted.Eagerly, null) + + override fun setSelectedCustomTileUrl(value: String?) { + scope.launch { + dataStore.edit { prefs -> + if (value == null) { + prefs.remove(KEY_SELECTED_CUSTOM_TILE_URL_PREF) + } else { + prefs[KEY_SELECTED_CUSTOM_TILE_URL_PREF] = value + } + } + } + } + + override val hiddenLayerUrls: StateFlow> = + dataStore.data + .map { it[KEY_HIDDEN_LAYER_URLS_PREF] ?: emptySet() } + .stateIn(scope, SharingStarted.Eagerly, emptySet()) + + override fun setHiddenLayerUrls(value: Set) { + scope.launch { dataStore.edit { it[KEY_HIDDEN_LAYER_URLS_PREF] = value } } + } + + override val cameraTargetLat: StateFlow = + dataStore.data.map { it[KEY_CAMERA_TARGET_LAT_PREF] ?: 0.0 }.stateIn(scope, SharingStarted.Eagerly, 0.0) + + override fun setCameraTargetLat(value: Double) { + scope.launch { dataStore.edit { it[KEY_CAMERA_TARGET_LAT_PREF] = value } } + } + + override val cameraTargetLng: StateFlow = + dataStore.data.map { it[KEY_CAMERA_TARGET_LNG_PREF] ?: 0.0 }.stateIn(scope, SharingStarted.Eagerly, 0.0) + + override fun setCameraTargetLng(value: Double) { + scope.launch { dataStore.edit { it[KEY_CAMERA_TARGET_LNG_PREF] = value } } + } + + override val cameraZoom: StateFlow = + dataStore.data.map { it[KEY_CAMERA_ZOOM_PREF] ?: 7f }.stateIn(scope, SharingStarted.Eagerly, 7f) + + override fun setCameraZoom(value: Float) { + scope.launch { dataStore.edit { it[KEY_CAMERA_ZOOM_PREF] = value } } + } + + override val cameraTilt: StateFlow = + dataStore.data.map { it[KEY_CAMERA_TILT_PREF] ?: 0f }.stateIn(scope, SharingStarted.Eagerly, 0f) + + override fun setCameraTilt(value: Float) { + scope.launch { dataStore.edit { it[KEY_CAMERA_TILT_PREF] = value } } + } + + override val cameraBearing: StateFlow = + dataStore.data.map { it[KEY_CAMERA_BEARING_PREF] ?: 0f }.stateIn(scope, SharingStarted.Eagerly, 0f) + + override fun setCameraBearing(value: Float) { + scope.launch { dataStore.edit { it[KEY_CAMERA_BEARING_PREF] = value } } + } + + override val networkMapLayers: StateFlow> = + dataStore.data + .map { it[KEY_NETWORK_MAP_LAYERS_PREF] ?: emptySet() } + .stateIn(scope, SharingStarted.Eagerly, emptySet()) + + override fun setNetworkMapLayers(value: Set) { + scope.launch { dataStore.edit { it[KEY_NETWORK_MAP_LAYERS_PREF] = value } } + } + + companion object { + val KEY_SELECTED_GOOGLE_MAP_TYPE_PREF = stringPreferencesKey("selected_google_map_type") + val KEY_SELECTED_CUSTOM_TILE_URL_PREF = stringPreferencesKey("selected_custom_tile_url") + val KEY_HIDDEN_LAYER_URLS_PREF = stringSetPreferencesKey("hidden_layer_urls") + val KEY_CAMERA_TARGET_LAT_PREF = doublePreferencesKey("camera_target_lat") + val KEY_CAMERA_TARGET_LNG_PREF = doublePreferencesKey("camera_target_lng") + val KEY_CAMERA_ZOOM_PREF = floatPreferencesKey("camera_zoom") + val KEY_CAMERA_TILT_PREF = floatPreferencesKey("camera_tilt") + val KEY_CAMERA_BEARING_PREF = floatPreferencesKey("camera_bearing") + val KEY_NETWORK_MAP_LAYERS_PREF = stringSetPreferencesKey("network_map_layers") + } } diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/DoublePrefDelegate.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/DoublePrefDelegate.kt deleted file mode 100644 index 0ecbb818e..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/DoublePrefDelegate.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs - -import android.content.SharedPreferences -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class DoublePrefDelegate( - private val preferences: SharedPreferences, - private val key: String, - private val defaultValue: Double, -) : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): Double = preferences - .getFloat(key, defaultValue.toFloat()) - .toDouble() // SharedPreferences doesn't have putDouble, so convert to float - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Double) { - preferences - .edit() - .putFloat(key, value.toFloat()) - .apply() // SharedPreferences doesn't have putDouble, so convert to float - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/FloatPrefDelegate.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/FloatPrefDelegate.kt deleted file mode 100644 index a2b12fcce..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/FloatPrefDelegate.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs - -import android.content.SharedPreferences -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -class FloatPrefDelegate( - private val preferences: SharedPreferences, - private val key: String, - private val defaultValue: Float, -) : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): Float = preferences.getFloat(key, defaultValue) - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Float) { - preferences.edit().putFloat(key, value).apply() - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/NullableStringPrefDelegate.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/NullableStringPrefDelegate.kt deleted file mode 100644 index f8fbd059f..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/NullableStringPrefDelegate.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs - -import android.content.SharedPreferences -import androidx.core.content.edit -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -/** - * A [ReadWriteProperty] delegate that provides concise, type-safe access to [SharedPreferences] for nullable strings. - * - * @param prefs The [SharedPreferences] instance to back the property. - * @param key The key used to store and retrieve the value. - * @param defaultValue The default value to return if no value is found. - */ -internal class NullableStringPrefDelegate( - private val prefs: SharedPreferences, - private val key: String, - private val defaultValue: String?, -) : ReadWriteProperty { - - override fun getValue(thisRef: Any?, property: KProperty<*>): String? = prefs.getString(key, defaultValue) - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { - prefs.edit { - when (value) { - null -> remove(key) - else -> putString(key, value) - } - } - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/PrefDelegate.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/PrefDelegate.kt deleted file mode 100644 index 28ce21b65..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/PrefDelegate.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs - -import android.content.SharedPreferences -import androidx.core.content.edit -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -/** - * A generic [ReadWriteProperty] delegate that provides concise, type-safe access to [SharedPreferences]. - * - * @param prefs The [SharedPreferences] instance to back the property. - * @param key The key used to store and retrieve the value. - * @param defaultValue The default value to return if no value is found. - * @throws IllegalArgumentException if the type is not supported. - */ -internal class PrefDelegate( - private val prefs: SharedPreferences, - private val key: String, - private val defaultValue: T, -) : ReadWriteProperty { - - @Suppress("UNCHECKED_CAST") - override fun getValue(thisRef: Any?, property: KProperty<*>): T = when (defaultValue) { - is String -> (prefs.getString(key, defaultValue) ?: defaultValue) as T - is Int -> prefs.getInt(key, defaultValue) as T - is Boolean -> prefs.getBoolean(key, defaultValue) as T - is Float -> prefs.getFloat(key, defaultValue) as T - is Long -> prefs.getLong(key, defaultValue) as T - else -> error("Unsupported type for key '$key': $defaultValue") - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - prefs.edit { - when (value) { - is String -> putString(key, value) - is Int -> putInt(key, value) - is Boolean -> putBoolean(key, value) - is Float -> putFloat(key, value) - is Long -> putLong(key, value) - else -> error("Unsupported type for key '$key': $value") - } - } - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/StringSetPrefDelegate.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/StringSetPrefDelegate.kt deleted file mode 100644 index 4cae1b099..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/StringSetPrefDelegate.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs - -import android.content.SharedPreferences -import androidx.core.content.edit -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -internal class StringSetPrefDelegate( - private val prefs: SharedPreferences, - private val key: String, - private val defaultValue: Set, -) : ReadWriteProperty> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Set = - prefs.getStringSet(key, defaultValue) ?: emptySet() - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set) = - prefs.edit { putStringSet(key, value) } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt deleted file mode 100644 index bb7592a1e..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefs.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.prefs.analytics - -import android.content.SharedPreferences -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.di.AnalyticsSharedPreferences -import org.meshtastic.core.prefs.di.AppSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.uuid.Uuid - -/** Interface for managing analytics-related preferences. */ -interface AnalyticsPrefs { - /** Preference for whether analytics collection is allowed by the user. */ - var analyticsAllowed: Boolean - - /** - * Provides a [Flow] that emits the current state of [analyticsAllowed] and subsequent changes. - * - * @return A [Flow] of [Boolean] indicating if analytics are allowed. - */ - fun getAnalyticsAllowedChangesFlow(): Flow - - /** Unique installation ID for analytics purposes. */ - val installId: String - - companion object { - /** Key for the analyticsAllowed preference. */ - const val KEY_ANALYTICS_ALLOWED = "allowed" - - /** Name of the SharedPreferences file where analytics preferences are stored. */ - const val ANALYTICS_PREFS_NAME = "analytics-prefs" - } -} - -@Singleton -class AnalyticsPrefsImpl -@Inject -constructor( - @AnalyticsSharedPreferences private val analyticsSharedPreferences: SharedPreferences, - @AppSharedPreferences appPrefs: SharedPreferences, -) : AnalyticsPrefs { - override var analyticsAllowed: Boolean by - PrefDelegate(analyticsSharedPreferences, AnalyticsPrefs.KEY_ANALYTICS_ALLOWED, false) - - private var _installId: String? by NullableStringPrefDelegate(appPrefs, "appPrefs_install_id", null) - - override val installId: String - get() = _installId ?: Uuid.random().toString().also { _installId = it } - - override fun getAnalyticsAllowedChangesFlow(): Flow = callbackFlow { - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == AnalyticsPrefs.KEY_ANALYTICS_ALLOWED) { - trySend(analyticsAllowed) - } - } - // Emit the initial value - trySend(analyticsAllowed) - analyticsSharedPreferences.registerOnSharedPreferenceChangeListener(listener) - awaitClose { analyticsSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt new file mode 100644 index 000000000..4fe087be0 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/analytics/AnalyticsPrefsImpl.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.analytics + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.AnalyticsDataStore +import org.meshtastic.core.prefs.di.AppDataStore +import org.meshtastic.core.repository.AnalyticsPrefs +import javax.inject.Inject +import javax.inject.Singleton +import kotlin.uuid.Uuid + +@Singleton +class AnalyticsPrefsImpl +@Inject +constructor( + @AnalyticsDataStore private val analyticsDataStore: DataStore, + @AppDataStore private val appDataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : AnalyticsPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val analyticsAllowed: StateFlow = + analyticsDataStore.data + .map { it[KEY_ANALYTICS_ALLOWED_PREF] ?: false } + .stateIn(scope, SharingStarted.Eagerly, false) + + override fun setAnalyticsAllowed(allowed: Boolean) { + scope.launch { analyticsDataStore.edit { prefs -> prefs[KEY_ANALYTICS_ALLOWED_PREF] = allowed } } + } + + override val installId: StateFlow = + appDataStore.data.map { it[KEY_INSTALL_ID_PREF] ?: "" }.stateIn(scope, SharingStarted.Eagerly, "") + + init { + scope.launch { + appDataStore.edit { prefs -> + if (prefs[KEY_INSTALL_ID_PREF] == null) { + prefs[KEY_INSTALL_ID_PREF] = Uuid.random().toString() + } + } + } + } + + companion object { + const val KEY_ANALYTICS_ALLOWED = "allowed" + const val KEY_INSTALL_ID = "appPrefs_install_id" + + val KEY_ANALYTICS_ALLOWED_PREF = booleanPreferencesKey(KEY_ANALYTICS_ALLOWED) + val KEY_INSTALL_ID_PREF = stringPreferencesKey(KEY_INSTALL_ID) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt index 2e5285be8..b1b8fbede 100644 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/di/PrefsModule.kt @@ -17,88 +17,92 @@ package org.meshtastic.core.prefs.di import android.content.Context -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.SharedPreferencesMigration +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.preferencesDataStoreFile import dagger.Binds import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent -import org.meshtastic.core.prefs.analytics.AnalyticsPrefs +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import org.meshtastic.core.prefs.analytics.AnalyticsPrefsImpl -import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs import org.meshtastic.core.prefs.emoji.CustomEmojiPrefsImpl -import org.meshtastic.core.prefs.filter.FilterPrefs import org.meshtastic.core.prefs.filter.FilterPrefsImpl -import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefs import org.meshtastic.core.prefs.homoglyph.HomoglyphPrefsImpl -import org.meshtastic.core.prefs.map.MapConsentPrefs import org.meshtastic.core.prefs.map.MapConsentPrefsImpl -import org.meshtastic.core.prefs.map.MapPrefs import org.meshtastic.core.prefs.map.MapPrefsImpl -import org.meshtastic.core.prefs.map.MapTileProviderPrefs import org.meshtastic.core.prefs.map.MapTileProviderPrefsImpl -import org.meshtastic.core.prefs.mesh.MeshPrefs import org.meshtastic.core.prefs.mesh.MeshPrefsImpl -import org.meshtastic.core.prefs.meshlog.MeshLogPrefs import org.meshtastic.core.prefs.meshlog.MeshLogPrefsImpl -import org.meshtastic.core.prefs.radio.RadioPrefs import org.meshtastic.core.prefs.radio.RadioPrefsImpl -import org.meshtastic.core.prefs.ui.UiPrefs import org.meshtastic.core.prefs.ui.UiPrefsImpl +import org.meshtastic.core.repository.AnalyticsPrefs +import org.meshtastic.core.repository.CustomEmojiPrefs +import org.meshtastic.core.repository.FilterPrefs +import org.meshtastic.core.repository.HomoglyphPrefs +import org.meshtastic.core.repository.MapConsentPrefs +import org.meshtastic.core.repository.MapPrefs +import org.meshtastic.core.repository.MapTileProviderPrefs +import org.meshtastic.core.repository.MeshLogPrefs +import org.meshtastic.core.repository.MeshPrefs +import org.meshtastic.core.repository.RadioPrefs +import org.meshtastic.core.repository.UiPrefs import javax.inject.Qualifier import javax.inject.Singleton -// These pref store qualifiers are internal to prevent prefs stores from being injected directly. -// Consuming code should always inject one of the prefs repositories. +@Qualifier +@Retention(AnnotationRetention.BINARY) +internal annotation class AnalyticsDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class AnalyticsSharedPreferences +internal annotation class HomoglyphEncodingDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class HomoglyphEncodingSharedPreferences +internal annotation class AppDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class AppSharedPreferences +internal annotation class CustomEmojiDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class CustomEmojiSharedPreferences +internal annotation class MapDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class MapSharedPreferences +internal annotation class MapConsentDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class MapConsentSharedPreferences +internal annotation class MapTileProviderDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class MapTileProviderSharedPreferences +internal annotation class MeshDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class MeshSharedPreferences +internal annotation class RadioDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class RadioSharedPreferences +internal annotation class UiDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class UiSharedPreferences +internal annotation class MeshLogDataStore @Qualifier @Retention(AnnotationRetention.BINARY) -internal annotation class MeshLogSharedPreferences - -@Qualifier -@Retention(AnnotationRetention.BINARY) -internal annotation class FilterSharedPreferences +internal annotation class FilterDataStore @Suppress("TooManyFunctions") @InstallIn(SingletonComponent::class) @@ -109,11 +113,6 @@ interface PrefsModule { @Binds fun bindHomoglyphEncodingPrefs(homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl): HomoglyphPrefs - @Binds - fun bindSharedHomoglyphPrefs( - homoglyphEncodingPrefsImpl: HomoglyphPrefsImpl, - ): org.meshtastic.core.repository.HomoglyphPrefs - @Binds fun bindCustomEmojiPrefs(customEmojiPrefsImpl: CustomEmojiPrefsImpl): CustomEmojiPrefs @Binds fun bindMapConsentPrefs(mapConsentPrefsImpl: MapConsentPrefsImpl): MapConsentPrefs @@ -133,77 +132,126 @@ interface PrefsModule { @Binds fun bindFilterPrefs(filterPrefsImpl: FilterPrefsImpl): FilterPrefs companion object { + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) @Provides @Singleton - @AnalyticsSharedPreferences - fun provideAnalyticsSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE) + @AnalyticsDataStore + fun provideAnalyticsDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "analytics-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("analytics_ds") }, + ) @Provides @Singleton - @HomoglyphEncodingSharedPreferences - fun provideHomoglyphEncodingSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("homoglyph-encoding-prefs", Context.MODE_PRIVATE) + @HomoglyphEncodingDataStore + fun provideHomoglyphEncodingDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "homoglyph-encoding-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("homoglyph_encoding_ds") }, + ) @Provides @Singleton - @AppSharedPreferences - fun provideAppSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("prefs", Context.MODE_PRIVATE) + @AppDataStore + fun provideAppDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("app_ds") }, + ) @Provides @Singleton - @CustomEmojiSharedPreferences - fun provideCustomEmojiSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("org.geeksville.emoji.prefs", Context.MODE_PRIVATE) + @CustomEmojiDataStore + fun provideCustomEmojiDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "org.geeksville.emoji.prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("custom_emoji_ds") }, + ) @Provides @Singleton - @MapSharedPreferences - fun provideMapSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("map_prefs", Context.MODE_PRIVATE) + @MapDataStore + fun provideMapDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_ds") }, + ) @Provides @Singleton - @MapConsentSharedPreferences - fun provideMapConsentSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("map_consent_preferences", Context.MODE_PRIVATE) + @MapConsentDataStore + fun provideMapConsentDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_consent_preferences")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_consent_ds") }, + ) @Provides @Singleton - @MapTileProviderSharedPreferences - fun provideMapTileProviderSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("map_tile_provider_prefs", Context.MODE_PRIVATE) + @MapTileProviderDataStore + fun provideMapTileProviderDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "map_tile_provider_prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("map_tile_provider_ds") }, + ) @Provides @Singleton - @MeshSharedPreferences - fun provideMeshSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("mesh-prefs", Context.MODE_PRIVATE) + @MeshDataStore + fun provideMeshDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "mesh-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("mesh_ds") }, + ) @Provides @Singleton - @RadioSharedPreferences - fun provideRadioSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE) + @RadioDataStore + fun provideRadioDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "radio-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("radio_ds") }, + ) @Provides @Singleton - @UiSharedPreferences - fun provideUiSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) + @UiDataStore + fun provideUiDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "ui-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("ui_ds") }, + ) @Provides @Singleton - @MeshLogSharedPreferences - fun provideMeshLogSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences("meshlog-prefs", Context.MODE_PRIVATE) + @MeshLogDataStore + fun provideMeshLogDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "meshlog-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("meshlog_ds") }, + ) @Provides @Singleton - @FilterSharedPreferences - fun provideFilterSharedPreferences(@ApplicationContext context: Context): SharedPreferences = - context.getSharedPreferences(FilterPrefs.FILTER_PREFS_NAME, Context.MODE_PRIVATE) + @FilterDataStore + fun provideFilterDataStore(@ApplicationContext context: Context): DataStore = + PreferenceDataStoreFactory.create( + migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")), + scope = scope, + produceFile = { context.preferencesDataStoreFile("filter_ds") }, + ) } } diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefs.kt deleted file mode 100644 index 986265590..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefs.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.emoji - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.di.CustomEmojiSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -interface CustomEmojiPrefs { - var customEmojiFrequency: String? -} - -@Singleton -class CustomEmojiPrefsImpl @Inject constructor(@CustomEmojiSharedPreferences prefs: SharedPreferences) : - CustomEmojiPrefs { - override var customEmojiFrequency: String? by NullableStringPrefDelegate(prefs, "pref_key_custom_emoji_freq", null) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt new file mode 100644 index 000000000..9bc7f1805 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/emoji/CustomEmojiPrefsImpl.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.emoji + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.CustomEmojiDataStore +import org.meshtastic.core.repository.CustomEmojiPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CustomEmojiPrefsImpl +@Inject +constructor( + @CustomEmojiDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : CustomEmojiPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val customEmojiFrequency: StateFlow = + dataStore.data.map { it[KEY_EMOJI_FREQ_PREF] }.stateIn(scope, SharingStarted.Eagerly, null) + + override fun setCustomEmojiFrequency(frequency: String?) { + scope.launch { + dataStore.edit { prefs -> + if (frequency == null) { + prefs.remove(KEY_EMOJI_FREQ_PREF) + } else { + prefs[KEY_EMOJI_FREQ_PREF] = frequency + } + } + } + } + + companion object { + const val KEY_EMOJI_FREQ = "pref_key_custom_emoji_freq" + val KEY_EMOJI_FREQ_PREF = stringPreferencesKey(KEY_EMOJI_FREQ) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefs.kt deleted file mode 100644 index aa76cba8d..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefs.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.prefs.filter - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.StringSetPrefDelegate -import org.meshtastic.core.prefs.di.FilterSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -/** Interface for managing message filter preferences. */ -interface FilterPrefs { - /** Whether message filtering is enabled. */ - var filterEnabled: Boolean - - /** Set of words to filter messages on. */ - var filterWords: Set - - companion object { - /** Key for the filterEnabled preference. */ - const val KEY_FILTER_ENABLED = "filter_enabled" - - /** Key for the filterWords preference. */ - const val KEY_FILTER_WORDS = "filter_words" - - /** Name of the SharedPreferences file where filter preferences are stored. */ - const val FILTER_PREFS_NAME = "filter-prefs" - } -} - -@Singleton -class FilterPrefsImpl @Inject constructor(@FilterSharedPreferences private val prefs: SharedPreferences) : FilterPrefs { - override var filterEnabled: Boolean by PrefDelegate(prefs, FilterPrefs.KEY_FILTER_ENABLED, false) - override var filterWords: Set by StringSetPrefDelegate(prefs, FilterPrefs.KEY_FILTER_WORDS, emptySet()) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt new file mode 100644 index 000000000..6ea9e24dd --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsImpl.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.filter + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringSetPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.FilterDataStore +import org.meshtastic.core.repository.FilterPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class FilterPrefsImpl +@Inject +constructor( + @FilterDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : FilterPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val filterEnabled: StateFlow = + dataStore.data.map { it[KEY_FILTER_ENABLED_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + + override fun setFilterEnabled(enabled: Boolean) { + scope.launch { dataStore.edit { prefs -> prefs[KEY_FILTER_ENABLED_PREF] = enabled } } + } + + override val filterWords: StateFlow> = + dataStore.data + .map { it[KEY_FILTER_WORDS_PREF] ?: emptySet() } + .stateIn(scope, SharingStarted.Eagerly, emptySet()) + + override fun setFilterWords(words: Set) { + scope.launch { dataStore.edit { prefs -> prefs[KEY_FILTER_WORDS_PREF] = words } } + } + + companion object { + const val KEY_FILTER_ENABLED = "filter_enabled" + const val KEY_FILTER_WORDS = "filter_words" + const val FILTER_PREFS_NAME = "filter-prefs" + + val KEY_FILTER_ENABLED_PREF = booleanPreferencesKey(KEY_FILTER_ENABLED) + val KEY_FILTER_WORDS_PREF = stringSetPreferencesKey(KEY_FILTER_WORDS) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt deleted file mode 100644 index b77b6fa97..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefs.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (c) 2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.prefs.homoglyph - -import android.content.SharedPreferences -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.di.HomoglyphEncodingSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton -import org.meshtastic.core.repository.HomoglyphPrefs as SharedHomoglyphPrefs - -interface HomoglyphPrefs : SharedHomoglyphPrefs { - - /** Preference for whether homoglyph encoding is enabled by the user. */ - override var homoglyphEncodingEnabled: Boolean - - /** - * Provides a [Flow] that emits the current state of [homoglyphEncodingEnabled] and subsequent changes. - * - * @return A [Flow] of [Boolean] indicating if homoglyph encoding is enabled. - */ - fun getHomoglyphEncodingEnabledChangesFlow(): Flow - - companion object { - /** Key for the homoglyphEncodingEnabled preference. */ - const val KEY_HOMOGLYPH_ENCODING_ENABLED = "enabled" - } -} - -@Singleton -class HomoglyphPrefsImpl -@Inject -constructor( - @HomoglyphEncodingSharedPreferences private val homoglyphEncodingSharedPreferences: SharedPreferences, -) : HomoglyphPrefs { - override var homoglyphEncodingEnabled: Boolean by - PrefDelegate(homoglyphEncodingSharedPreferences, HomoglyphPrefs.KEY_HOMOGLYPH_ENCODING_ENABLED, false) - - override fun getHomoglyphEncodingEnabledChangesFlow(): Flow = callbackFlow { - val listener = - SharedPreferences.OnSharedPreferenceChangeListener { _, key -> - if (key == HomoglyphPrefs.KEY_HOMOGLYPH_ENCODING_ENABLED) { - trySend(homoglyphEncodingEnabled) - } - } - // Emit the initial value - trySend(homoglyphEncodingEnabled) - homoglyphEncodingSharedPreferences.registerOnSharedPreferenceChangeListener(listener) - awaitClose { homoglyphEncodingSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) } - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt new file mode 100644 index 000000000..42b4f8faa --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/homoglyph/HomoglyphPrefsImpl.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.homoglyph + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.HomoglyphEncodingDataStore +import org.meshtastic.core.repository.HomoglyphPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class HomoglyphPrefsImpl +@Inject +constructor( + @HomoglyphEncodingDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : HomoglyphPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val homoglyphEncodingEnabled: StateFlow = + dataStore.data.map { it[KEY_ENABLED_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + + override fun setHomoglyphEncodingEnabled(enabled: Boolean) { + scope.launch { dataStore.edit { prefs -> prefs[KEY_ENABLED_PREF] = enabled } } + } + + companion object { + const val KEY_ENABLED = "enabled" + val KEY_ENABLED_PREF = booleanPreferencesKey(KEY_ENABLED) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefs.kt deleted file mode 100644 index ae1a76890..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefs.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.map - -import android.content.SharedPreferences -import androidx.core.content.edit -import org.meshtastic.core.prefs.di.MapConsentSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -interface MapConsentPrefs { - fun shouldReportLocation(nodeNum: Int?): Boolean - - fun setShouldReportLocation(nodeNum: Int?, value: Boolean) -} - -@Singleton -class MapConsentPrefsImpl @Inject constructor(@MapConsentSharedPreferences private val prefs: SharedPreferences) : - MapConsentPrefs { - override fun shouldReportLocation(nodeNum: Int?) = prefs.getBoolean(nodeNum.toString(), false) - - override fun setShouldReportLocation(nodeNum: Int?, value: Boolean) { - prefs.edit { putBoolean(nodeNum.toString(), value) } - } -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt new file mode 100644 index 000000000..bf22eb27d --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapConsentPrefsImpl.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.map + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.MapConsentDataStore +import org.meshtastic.core.repository.MapConsentPrefs +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MapConsentPrefsImpl +@Inject +constructor( + @MapConsentDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : MapConsentPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + private val consentFlows = ConcurrentHashMap>() + + override fun shouldReportLocation(nodeNum: Int?): StateFlow = consentFlows.getOrPut(nodeNum) { + val key = booleanPreferencesKey(nodeNum.toString()) + dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + } + + override fun setShouldReportLocation(nodeNum: Int?, report: Boolean) { + scope.launch { dataStore.edit { prefs -> prefs[booleanPreferencesKey(nodeNum.toString())] = report } } + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefs.kt deleted file mode 100644 index 6edabbc0c..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefs.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.map - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.di.MapSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -/** Interface for general map prefs. For Google-specific prefs, see GoogleMapsPrefs. */ -interface MapPrefs { - var mapStyle: Int - var showOnlyFavorites: Boolean - var showWaypointsOnMap: Boolean - var showPrecisionCircleOnMap: Boolean - var lastHeardFilter: Long - var lastHeardTrackFilter: Long -} - -@Singleton -class MapPrefsImpl @Inject constructor(@MapSharedPreferences prefs: SharedPreferences) : MapPrefs { - override var mapStyle: Int by PrefDelegate(prefs, "map_style_id", 0) - override var showOnlyFavorites: Boolean by PrefDelegate(prefs, "show_only_favorites", false) - override var showWaypointsOnMap: Boolean by PrefDelegate(prefs, "show_waypoints", true) - override var showPrecisionCircleOnMap: Boolean by PrefDelegate(prefs, "show_precision_circle", true) - override var lastHeardFilter: Long by PrefDelegate(prefs, "last_heard_filter", 0L) - override var lastHeardTrackFilter: Long by PrefDelegate(prefs, "last_heard_track_filter", 0L) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt new file mode 100644 index 000000000..52167812f --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapPrefsImpl.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.map + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.longPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.MapDataStore +import org.meshtastic.core.repository.MapPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MapPrefsImpl +@Inject +constructor( + @MapDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : MapPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val mapStyle: StateFlow = + dataStore.data.map { it[KEY_MAP_STYLE_PREF] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0) + + override fun setMapStyle(value: Int) { + scope.launch { dataStore.edit { it[KEY_MAP_STYLE_PREF] = value } } + } + + override val showOnlyFavorites: StateFlow = + dataStore.data.map { it[KEY_SHOW_ONLY_FAVORITES_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + + override fun setShowOnlyFavorites(value: Boolean) { + scope.launch { dataStore.edit { it[KEY_SHOW_ONLY_FAVORITES_PREF] = value } } + } + + override val showWaypointsOnMap: StateFlow = + dataStore.data.map { it[KEY_SHOW_WAYPOINTS_PREF] ?: true }.stateIn(scope, SharingStarted.Eagerly, true) + + override fun setShowWaypointsOnMap(value: Boolean) { + scope.launch { dataStore.edit { it[KEY_SHOW_WAYPOINTS_PREF] = value } } + } + + override val showPrecisionCircleOnMap: StateFlow = + dataStore.data.map { it[KEY_SHOW_PRECISION_CIRCLE_PREF] ?: true }.stateIn(scope, SharingStarted.Eagerly, true) + + override fun setShowPrecisionCircleOnMap(value: Boolean) { + scope.launch { dataStore.edit { it[KEY_SHOW_PRECISION_CIRCLE_PREF] = value } } + } + + override val lastHeardFilter: StateFlow = + dataStore.data.map { it[KEY_LAST_HEARD_FILTER_PREF] ?: 0L }.stateIn(scope, SharingStarted.Eagerly, 0L) + + override fun setLastHeardFilter(value: Long) { + scope.launch { dataStore.edit { it[KEY_LAST_HEARD_FILTER_PREF] = value } } + } + + override val lastHeardTrackFilter: StateFlow = + dataStore.data.map { it[KEY_LAST_HEARD_TRACK_FILTER_PREF] ?: 0L }.stateIn(scope, SharingStarted.Eagerly, 0L) + + override fun setLastHeardTrackFilter(value: Long) { + scope.launch { dataStore.edit { it[KEY_LAST_HEARD_TRACK_FILTER_PREF] = value } } + } + + companion object { + val KEY_MAP_STYLE_PREF = intPreferencesKey("map_style_id") + val KEY_SHOW_ONLY_FAVORITES_PREF = booleanPreferencesKey("show_only_favorites") + val KEY_SHOW_WAYPOINTS_PREF = booleanPreferencesKey("show_waypoints") + val KEY_SHOW_PRECISION_CIRCLE_PREF = booleanPreferencesKey("show_precision_circle") + val KEY_LAST_HEARD_FILTER_PREF = longPreferencesKey("last_heard_filter") + val KEY_LAST_HEARD_TRACK_FILTER_PREF = longPreferencesKey("last_heard_track_filter") + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefs.kt deleted file mode 100644 index 9c86a4b13..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefs.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.map - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.di.MapTileProviderSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -interface MapTileProviderPrefs { - var customTileProviders: String? -} - -@Singleton -class MapTileProviderPrefsImpl @Inject constructor(@MapTileProviderSharedPreferences prefs: SharedPreferences) : - MapTileProviderPrefs { - override var customTileProviders: String? by NullableStringPrefDelegate(prefs, "custom_tile_providers", null) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt new file mode 100644 index 000000000..c3a686e97 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/map/MapTileProviderPrefsImpl.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.map + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.MapTileProviderDataStore +import org.meshtastic.core.repository.MapTileProviderPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MapTileProviderPrefsImpl +@Inject +constructor( + @MapTileProviderDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : MapTileProviderPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val customTileProviders: StateFlow = + dataStore.data.map { it[KEY_CUSTOM_PROVIDERS_PREF] }.stateIn(scope, SharingStarted.Eagerly, null) + + override fun setCustomTileProviders(providers: String?) { + scope.launch { + dataStore.edit { prefs -> + if (providers == null) { + prefs.remove(KEY_CUSTOM_PROVIDERS_PREF) + } else { + prefs[KEY_CUSTOM_PROVIDERS_PREF] = providers + } + } + } + } + + companion object { + const val KEY_CUSTOM_PROVIDERS = "custom_tile_providers" + val KEY_CUSTOM_PROVIDERS_PREF = stringPreferencesKey(KEY_CUSTOM_PROVIDERS) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefs.kt deleted file mode 100644 index fb121a692..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefs.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.mesh - -import android.content.SharedPreferences -import androidx.core.content.edit -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.di.MeshSharedPreferences -import java.util.Locale -import javax.inject.Inject -import javax.inject.Singleton - -interface MeshPrefs { - var deviceAddress: String? - - fun shouldProvideNodeLocation(nodeNum: Int?): Boolean - - fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) - - fun getStoreForwardLastRequest(address: String?): Int - - fun setStoreForwardLastRequest(address: String?, value: Int) -} - -@Singleton -class MeshPrefsImpl @Inject constructor(@MeshSharedPreferences private val prefs: SharedPreferences) : MeshPrefs { - override var deviceAddress: String? by NullableStringPrefDelegate(prefs, "device_address", NO_DEVICE_SELECTED) - - override fun shouldProvideNodeLocation(nodeNum: Int?): Boolean = - prefs.getBoolean(provideLocationKey(nodeNum), false) - - override fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) { - prefs.edit { putBoolean(provideLocationKey(nodeNum), value) } - } - - override fun getStoreForwardLastRequest(address: String?): Int = prefs.getInt(storeForwardKey(address), 0) - - override fun setStoreForwardLastRequest(address: String?, value: Int) { - prefs.edit { - if (value <= 0) { - remove(storeForwardKey(address)) - } else { - putInt(storeForwardKey(address), value) - } - } - } - - private fun provideLocationKey(nodeNum: Int?) = "provide-location-$nodeNum" - - private fun storeForwardKey(address: String?): String = "store-forward-last-request-${normalizeAddress(address)}" - - private fun normalizeAddress(address: String?): String { - val raw = address?.trim()?.takeIf { it.isNotEmpty() } - return when { - raw == null -> "DEFAULT" - raw.equals(NO_DEVICE_SELECTED, ignoreCase = true) -> "DEFAULT" - else -> raw.uppercase(Locale.US).replace(":", "") - } - } -} - -private const val NO_DEVICE_SELECTED = "n" diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt new file mode 100644 index 000000000..c247788f2 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/mesh/MeshPrefsImpl.kt @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.mesh + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.MeshDataStore +import org.meshtastic.core.repository.MeshPrefs +import java.util.Locale +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MeshPrefsImpl +@Inject +constructor( + @MeshDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : MeshPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + private val locationFlows = ConcurrentHashMap>() + private val storeForwardFlows = ConcurrentHashMap>() + + override val deviceAddress: StateFlow = + dataStore.data + .map { it[KEY_DEVICE_ADDRESS_PREF] ?: NO_DEVICE_SELECTED } + .stateIn(scope, SharingStarted.Eagerly, NO_DEVICE_SELECTED) + + override fun setDeviceAddress(address: String?) { + scope.launch { + dataStore.edit { prefs -> + if (address == null) { + prefs.remove(KEY_DEVICE_ADDRESS_PREF) + } else { + prefs[KEY_DEVICE_ADDRESS_PREF] = address + } + } + } + } + + override fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow = locationFlows.getOrPut(nodeNum) { + val key = booleanPreferencesKey(provideLocationKey(nodeNum)) + dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + } + + override fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) { + scope.launch { dataStore.edit { prefs -> prefs[booleanPreferencesKey(provideLocationKey(nodeNum))] = value } } + } + + override fun getStoreForwardLastRequest(address: String?): StateFlow = storeForwardFlows.getOrPut(address) { + val key = intPreferencesKey(storeForwardKey(address)) + dataStore.data.map { it[key] ?: 0 }.stateIn(scope, SharingStarted.Eagerly, 0) + } + + override fun setStoreForwardLastRequest(address: String?, value: Int) { + scope.launch { + dataStore.edit { prefs -> + val key = intPreferencesKey(storeForwardKey(address)) + if (value <= 0) { + prefs.remove(key) + } else { + prefs[key] = value + } + } + } + } + + private fun provideLocationKey(nodeNum: Int?) = "provide-location-$nodeNum" + + private fun storeForwardKey(address: String?): String = "store-forward-last-request-${normalizeAddress(address)}" + + private fun normalizeAddress(address: String?): String { + val raw = address?.trim()?.takeIf { it.isNotEmpty() } + return when { + raw == null -> "DEFAULT" + raw.equals(NO_DEVICE_SELECTED, ignoreCase = true) -> "DEFAULT" + else -> raw.uppercase(Locale.US).replace(":", "") + } + } + + companion object { + val KEY_DEVICE_ADDRESS_PREF = stringPreferencesKey("device_address") + } +} + +private const val NO_DEVICE_SELECTED = "n" diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefs.kt deleted file mode 100644 index f110cf6aa..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefs.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.prefs.meshlog - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.di.MeshLogSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -interface MeshLogPrefs { - var retentionDays: Int - var loggingEnabled: Boolean - - companion object { - const val RETENTION_DAYS_KEY = "meshlog_retention_days" - const val LOGGING_ENABLED_KEY = "meshlog_logging_enabled" - const val DEFAULT_RETENTION_DAYS = 30 - const val DEFAULT_LOGGING_ENABLED = true - const val MIN_RETENTION_DAYS = -1 // -1 == keep last hour - const val MAX_RETENTION_DAYS = 365 - const val NEVER_CLEAR_RETENTION_DAYS = 0 - const val ONE_HOUR_RETENTION_DAYS = -1 - } -} - -@Singleton -class MeshLogPrefsImpl @Inject constructor(@MeshLogSharedPreferences private val prefs: SharedPreferences) : - MeshLogPrefs { - override var retentionDays: Int by - PrefDelegate( - prefs = prefs, - key = MeshLogPrefs.RETENTION_DAYS_KEY, - defaultValue = MeshLogPrefs.DEFAULT_RETENTION_DAYS, - ) - override var loggingEnabled: Boolean by - PrefDelegate( - prefs = prefs, - key = MeshLogPrefs.LOGGING_ENABLED_KEY, - defaultValue = MeshLogPrefs.DEFAULT_LOGGING_ENABLED, - ) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt new file mode 100644 index 000000000..a10c27da8 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/meshlog/MeshLogPrefsImpl.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.meshlog + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.MeshLogDataStore +import org.meshtastic.core.repository.MeshLogPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MeshLogPrefsImpl +@Inject +constructor( + @MeshLogDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : MeshLogPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val retentionDays: StateFlow = + dataStore.data + .map { it[KEY_RETENTION_DAYS_PREF] ?: DEFAULT_RETENTION_DAYS } + .stateIn(scope, SharingStarted.Eagerly, DEFAULT_RETENTION_DAYS) + + override fun setRetentionDays(days: Int) { + scope.launch { dataStore.edit { it[KEY_RETENTION_DAYS_PREF] = days } } + } + + override val loggingEnabled: StateFlow = + dataStore.data + .map { it[KEY_LOGGING_ENABLED_PREF] ?: DEFAULT_LOGGING_ENABLED } + .stateIn(scope, SharingStarted.Eagerly, DEFAULT_LOGGING_ENABLED) + + override fun setLoggingEnabled(enabled: Boolean) { + scope.launch { dataStore.edit { it[KEY_LOGGING_ENABLED_PREF] = enabled } } + } + + companion object { + const val RETENTION_DAYS_KEY = "meshlog_retention_days" + const val LOGGING_ENABLED_KEY = "meshlog_logging_enabled" + const val DEFAULT_RETENTION_DAYS = 30 + const val DEFAULT_LOGGING_ENABLED = true + + val KEY_RETENTION_DAYS_PREF = intPreferencesKey(RETENTION_DAYS_KEY) + val KEY_LOGGING_ENABLED_PREF = booleanPreferencesKey(LOGGING_ENABLED_KEY) + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefs.kt deleted file mode 100644 index baa049ff6..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefs.kt +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.radio - -import android.content.SharedPreferences -import org.meshtastic.core.prefs.NullableStringPrefDelegate -import org.meshtastic.core.prefs.di.RadioSharedPreferences -import javax.inject.Inject -import javax.inject.Singleton - -interface RadioPrefs { - var devAddr: String? -} - -fun RadioPrefs.isBle() = devAddr?.startsWith("x") == true - -fun RadioPrefs.isSerial() = devAddr?.startsWith("s") == true - -fun RadioPrefs.isMock() = devAddr?.startsWith("m") == true - -fun RadioPrefs.isTcp() = devAddr?.startsWith("t") == true - -fun RadioPrefs.isNoop() = devAddr?.startsWith("n") == true - -@Singleton -class RadioPrefsImpl @Inject constructor(@RadioSharedPreferences prefs: SharedPreferences) : RadioPrefs { - override var devAddr: String? by NullableStringPrefDelegate(prefs, "devAddr2", null) -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt new file mode 100644 index 000000000..916bb892c --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/radio/RadioPrefsImpl.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.radio + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.RadioDataStore +import org.meshtastic.core.repository.RadioPrefs +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RadioPrefsImpl +@Inject +constructor( + @RadioDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : RadioPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + override val devAddr: StateFlow = + dataStore.data.map { it[KEY_DEV_ADDR_PREF] }.stateIn(scope, SharingStarted.Eagerly, null) + + override fun setDevAddr(address: String?) { + scope.launch { + dataStore.edit { prefs -> + if (address == null) { + prefs.remove(KEY_DEV_ADDR_PREF) + } else { + prefs[KEY_DEV_ADDR_PREF] = address + } + } + } + } + + companion object { + val KEY_DEV_ADDR_PREF = stringPreferencesKey("devAddr2") + } +} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt deleted file mode 100644 index 138a4afa5..000000000 --- a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefs.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.core.prefs.ui - -import android.content.SharedPreferences -import androidx.core.content.edit -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import org.meshtastic.core.prefs.PrefDelegate -import org.meshtastic.core.prefs.di.UiSharedPreferences -import java.util.concurrent.ConcurrentHashMap -import javax.inject.Inject -import javax.inject.Singleton - -interface UiPrefs { - var hasShownNotPairedWarning: Boolean - var showQuickChat: Boolean - - fun shouldProvideNodeLocation(nodeNum: Int): StateFlow - - fun setShouldProvideNodeLocation(nodeNum: Int, value: Boolean) -} - -@Singleton -class UiPrefsImpl @Inject constructor(@UiSharedPreferences private val prefs: SharedPreferences) : UiPrefs { - - // Maps nodeNum to a flow for the for the "provide-location-nodeNum" pref - private val provideNodeLocationFlows = ConcurrentHashMap>() - - private val sharedPreferencesListener = - SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key -> - when (key) { - // Check if the changed key is one of our node location keys - else -> - provideNodeLocationFlows.keys.forEach { nodeNum -> - if (key == provideLocationKey(nodeNum)) { - val newValue = sharedPreferences.getBoolean(key, false) - provideNodeLocationFlows[nodeNum]?.tryEmit(newValue) - } - } - } - } - - init { - prefs.registerOnSharedPreferenceChangeListener(sharedPreferencesListener) - } - - override var hasShownNotPairedWarning: Boolean by PrefDelegate(prefs, "has_shown_not_paired_warning", false) - override var showQuickChat: Boolean by PrefDelegate(prefs, "show-quick-chat", false) - - override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow = provideNodeLocationFlows - .getOrPut(nodeNum) { MutableStateFlow(prefs.getBoolean(provideLocationKey(nodeNum), false)) } - .asStateFlow() - - override fun setShouldProvideNodeLocation(nodeNum: Int, value: Boolean) { - prefs.edit { putBoolean(provideLocationKey(nodeNum), value) } - provideNodeLocationFlows[nodeNum]?.tryEmit(value) - } - - private fun provideLocationKey(nodeNum: Int) = "provide-location-$nodeNum" -} diff --git a/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt new file mode 100644 index 000000000..13c8ed336 --- /dev/null +++ b/core/prefs/src/main/kotlin/org/meshtastic/core/prefs/ui/UiPrefsImpl.kt @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.prefs.ui + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.prefs.di.UiDataStore +import org.meshtastic.core.repository.UiPrefs +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class UiPrefsImpl +@Inject +constructor( + @UiDataStore private val dataStore: DataStore, + dispatchers: CoroutineDispatchers, +) : UiPrefs { + private val scope = CoroutineScope(SupervisorJob() + dispatchers.default) + + // Maps nodeNum to a flow for the for the "provide-location-nodeNum" pref + private val provideNodeLocationFlows = ConcurrentHashMap>() + + override val hasShownNotPairedWarning: StateFlow = + dataStore.data + .map { it[KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF] ?: false } + .stateIn(scope, SharingStarted.Eagerly, false) + + override fun setHasShownNotPairedWarning(value: Boolean) { + scope.launch { dataStore.edit { it[KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF] = value } } + } + + override val showQuickChat: StateFlow = + dataStore.data.map { it[KEY_SHOW_QUICK_CHAT_PREF] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + + override fun setShowQuickChat(value: Boolean) { + scope.launch { dataStore.edit { it[KEY_SHOW_QUICK_CHAT_PREF] = value } } + } + + override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow = + provideNodeLocationFlows.getOrPut(nodeNum) { + val key = booleanPreferencesKey(provideLocationKey(nodeNum)) + dataStore.data.map { it[key] ?: false }.stateIn(scope, SharingStarted.Eagerly, false) + } + + override fun setShouldProvideNodeLocation(nodeNum: Int, value: Boolean) { + scope.launch { dataStore.edit { it[booleanPreferencesKey(provideLocationKey(nodeNum))] = value } } + } + + private fun provideLocationKey(nodeNum: Int) = "provide-location-$nodeNum" + + companion object { + val KEY_HAS_SHOWN_NOT_PAIRED_WARNING_PREF = booleanPreferencesKey("has_shown_not_paired_warning") + val KEY_SHOW_QUICK_CHAT_PREF = booleanPreferencesKey("show-quick-chat") + } +} diff --git a/core/prefs/src/test/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt b/core/prefs/src/test/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt index 37db3f2ef..efe1dacd8 100644 --- a/core/prefs/src/test/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt +++ b/core/prefs/src/test/kotlin/org/meshtastic/core/prefs/filter/FilterPrefsTest.kt @@ -16,51 +16,61 @@ */ package org.meshtastic.core.prefs.filter -import android.content.SharedPreferences +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences import io.mockk.every import io.mockk.mockk -import io.mockk.verify +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.FilterPrefs class FilterPrefsTest { - private lateinit var sharedPreferences: SharedPreferences - private lateinit var editor: SharedPreferences.Editor + @get:Rule val tmpFolder: TemporaryFolder = TemporaryFolder.builder().assureDeletion().build() + + private lateinit var dataStore: DataStore private lateinit var filterPrefs: FilterPrefs + private lateinit var dispatchers: CoroutineDispatchers + + private val testDispatcher = UnconfinedTestDispatcher() + private val testScope = TestScope(testDispatcher) @Before fun setup() { - editor = mockk(relaxed = true) - sharedPreferences = mockk { - every { getBoolean(FilterPrefs.KEY_FILTER_ENABLED, false) } returns false - every { getStringSet(FilterPrefs.KEY_FILTER_WORDS, emptySet()) } returns emptySet() - every { edit() } returns editor - } - filterPrefs = FilterPrefsImpl(sharedPreferences) + dataStore = + PreferenceDataStoreFactory.create( + scope = testScope, + produceFile = { tmpFolder.newFile("test.preferences_pb") }, + ) + dispatchers = mockk { every { default } returns testDispatcher } + filterPrefs = FilterPrefsImpl(dataStore, dispatchers) + } + + @Test fun `filterEnabled defaults to false`() = testScope.runTest { assertFalse(filterPrefs.filterEnabled.value) } + + @Test + fun `filterWords defaults to empty set`() = + testScope.runTest { assertTrue(filterPrefs.filterWords.value.isEmpty()) } + + @Test + fun `setting filterEnabled updates preference`() = testScope.runTest { + filterPrefs.setFilterEnabled(true) + assertTrue(filterPrefs.filterEnabled.value) } @Test - fun `filterEnabled defaults to false`() { - assertFalse(filterPrefs.filterEnabled) - } - - @Test - fun `filterWords defaults to empty set`() { - assertTrue(filterPrefs.filterWords.isEmpty()) - } - - @Test - fun `setting filterEnabled updates preference`() { - filterPrefs.filterEnabled = true - verify { editor.putBoolean(FilterPrefs.KEY_FILTER_ENABLED, true) } - } - - @Test - fun `setting filterWords updates preference`() { + fun `setting filterWords updates preference`() = testScope.runTest { val words = setOf("test", "word") - filterPrefs.filterWords = words - verify { editor.putStringSet(FilterPrefs.KEY_FILTER_WORDS, words) } + filterPrefs.setFilterWords(words) + assertEquals(words, filterPrefs.filterWords.value) } } diff --git a/core/repository/build.gradle.kts b/core/repository/build.gradle.kts index 778dde947..44e49f491 100644 --- a/core/repository/build.gradle.kts +++ b/core/repository/build.gradle.kts @@ -26,6 +26,7 @@ kotlin { api(projects.core.model) api(projects.core.proto) implementation(projects.core.common) + implementation(projects.core.database) implementation(libs.kotlinx.coroutines.core) implementation(libs.kermit) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt new file mode 100644 index 000000000..82f7ff86b --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/AppPreferences.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.StateFlow + +/** Reactive interface for analytics-related preferences. */ +interface AnalyticsPrefs { + val analyticsAllowed: StateFlow + + fun setAnalyticsAllowed(allowed: Boolean) + + val installId: StateFlow +} + +/** Reactive interface for homoglyph encoding preferences. */ +interface HomoglyphPrefs { + val homoglyphEncodingEnabled: StateFlow + + fun setHomoglyphEncodingEnabled(enabled: Boolean) +} + +/** Reactive interface for message filtering preferences. */ +interface FilterPrefs { + val filterEnabled: StateFlow + + fun setFilterEnabled(enabled: Boolean) + + val filterWords: StateFlow> + + fun setFilterWords(words: Set) +} + +/** Reactive interface for mesh log preferences. */ +interface MeshLogPrefs { + val retentionDays: StateFlow + + fun setRetentionDays(days: Int) + + val loggingEnabled: StateFlow + + fun setLoggingEnabled(enabled: Boolean) + + companion object { + const val DEFAULT_RETENTION_DAYS = 30 + const val MIN_RETENTION_DAYS = -1 + const val MAX_RETENTION_DAYS = 365 + } +} + +/** Reactive interface for emoji preferences. */ +interface CustomEmojiPrefs { + val customEmojiFrequency: StateFlow + + fun setCustomEmojiFrequency(frequency: String?) +} + +/** Reactive interface for general UI preferences. */ +interface UiPrefs { + val hasShownNotPairedWarning: StateFlow + + fun setHasShownNotPairedWarning(shown: Boolean) + + val showQuickChat: StateFlow + + fun setShowQuickChat(show: Boolean) + + fun shouldProvideNodeLocation(nodeNum: Int): StateFlow + + fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean) +} + +/** Reactive interface for general map preferences. */ +interface MapPrefs { + val mapStyle: StateFlow + + fun setMapStyle(style: Int) + + val showOnlyFavorites: StateFlow + + fun setShowOnlyFavorites(show: Boolean) + + val showWaypointsOnMap: StateFlow + + fun setShowWaypointsOnMap(show: Boolean) + + val showPrecisionCircleOnMap: StateFlow + + fun setShowPrecisionCircleOnMap(show: Boolean) + + val lastHeardFilter: StateFlow + + fun setLastHeardFilter(seconds: Long) + + val lastHeardTrackFilter: StateFlow + + fun setLastHeardTrackFilter(seconds: Long) +} + +/** Reactive interface for map consent. */ +interface MapConsentPrefs { + fun shouldReportLocation(nodeNum: Int?): StateFlow + + fun setShouldReportLocation(nodeNum: Int?, report: Boolean) +} + +/** Reactive interface for map tile provider settings. */ +interface MapTileProviderPrefs { + val customTileProviders: StateFlow + + fun setCustomTileProviders(providers: String?) +} + +/** Reactive interface for radio settings. */ +interface RadioPrefs { + val devAddr: StateFlow + + fun setDevAddr(address: String?) +} + +fun RadioPrefs.isBle() = devAddr.value?.startsWith("x") == true + +fun RadioPrefs.isSerial() = devAddr.value?.startsWith("s") == true + +fun RadioPrefs.isMock() = devAddr.value?.startsWith("m") == true + +fun RadioPrefs.isTcp() = devAddr.value?.startsWith("t") == true + +fun RadioPrefs.isNoop() = devAddr.value?.startsWith("n") == true + +/** Reactive interface for mesh connection settings. */ +interface MeshPrefs { + val deviceAddress: StateFlow + + fun setDeviceAddress(address: String?) + + fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow + + fun setShouldProvideNodeLocation(nodeNum: Int?, provide: Boolean) + + fun getStoreForwardLastRequest(address: String?): StateFlow + + fun setStoreForwardLastRequest(address: String?, timestamp: Int) +} + +/** Consolidated interface for all application preferences. */ +interface AppPreferences { + val analytics: AnalyticsPrefs + val homoglyph: HomoglyphPrefs + val filter: FilterPrefs + val meshLog: MeshLogPrefs + val emoji: CustomEmojiPrefs + val ui: UiPrefs + val map: MapPrefs + val mapConsent: MapConsentPrefs + val mapTileProvider: MapTileProviderPrefs + val radio: RadioPrefs + val mesh: MeshPrefs +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt deleted file mode 100644 index 4c497af0b..000000000 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/HomoglyphPrefs.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright (c) 2025-2026 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ -package org.meshtastic.core.repository - -interface HomoglyphPrefs { - val homoglyphEncodingEnabled: Boolean -} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt new file mode 100644 index 000000000..94f750032 --- /dev/null +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshLogRepository.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2025-2026 Meshtastic LLC + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.meshtastic.core.repository + +import kotlinx.coroutines.flow.Flow +import org.meshtastic.core.database.entity.MeshLog +import org.meshtastic.proto.MeshPacket +import org.meshtastic.proto.MyNodeInfo +import org.meshtastic.proto.PortNum +import org.meshtastic.proto.Telemetry + +/** + * Repository interface for managing and retrieving logs from the database. + * + * This component provides access to the application's message log, telemetry history, and debug records. It supports + * reactive queries for packets, telemetry data, and node-specific logs. + * + * This interface is shared across platforms via Kotlin Multiplatform (KMP). + */ +@Suppress("TooManyFunctions") +interface MeshLogRepository { + /** Retrieves all [MeshLog]s in the database, up to [maxItem]. */ + fun getAllLogs(maxItem: Int = DEFAULT_MAX_LOGS): Flow> + + /** Retrieves all [MeshLog]s in the database in the order they were received. */ + fun getAllLogsInReceiveOrder(maxItem: Int = DEFAULT_MAX_LOGS): Flow> + + /** Retrieves all [MeshLog]s in the database without any limit. */ + fun getAllLogsUnbounded(): Flow> + + /** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */ + fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> + + /** Retrieves all [MeshLog]s containing [MeshPacket]s for a specific [nodeNum]. */ + fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = -1): Flow> + + /** Retrieves telemetry history for a specific node, automatically handling local node redirection. */ + fun getTelemetryFrom(nodeNum: Int): Flow> + + /** + * Retrieves all outgoing request logs for a specific [targetNodeNum] and [portNum]. + * + * A request log is defined as an outgoing packet where `want_response` is true. + */ + fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow> + + /** Returns the cached [MyNodeInfo] from the system logs. */ + fun getMyNodeInfo(): Flow + + /** Persists a new log entry to the database. */ + suspend fun insert(log: MeshLog) + + /** Clears all logs from the database. */ + suspend fun deleteAll() + + /** Deletes a specific log entry by its [uuid]. */ + suspend fun deleteLog(uuid: String) + + /** Deletes all logs associated with a specific [nodeNum] and [portNum]. */ + suspend fun deleteLogs(nodeNum: Int, portNum: Int) + + /** Prunes the log database based on the configured [retentionDays]. */ + suspend fun deleteLogsOlderThan(retentionDays: Int) + + companion object { + const val DEFAULT_MAX_LOGS = 5000 + } +} diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt index 6aff09473..714179729 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/usecase/SendMessageUseCase.kt @@ -89,7 +89,7 @@ class SendMessageUseCase( // Apply homoglyph encoding val finalMessageText = - if (homoglyphEncodingPrefs.homoglyphEncodingEnabled) { + if (homoglyphEncodingPrefs.homoglyphEncodingEnabled.value) { HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(text) } else { text diff --git a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt index 27c727612..8a30006d8 100644 --- a/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt +++ b/core/ui/src/main/kotlin/org/meshtastic/core/ui/emoji/EmojiPickerViewModel.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025 Meshtastic LLC + * Copyright (c) 2025-2026 Meshtastic LLC * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -14,20 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - package org.meshtastic.core.ui.emoji import androidx.lifecycle.ViewModel import dagger.hilt.android.lifecycle.HiltViewModel -import org.meshtastic.core.prefs.emoji.CustomEmojiPrefs +import org.meshtastic.core.repository.CustomEmojiPrefs import javax.inject.Inject @HiltViewModel class EmojiPickerViewModel @Inject constructor(private val customEmojiPrefs: CustomEmojiPrefs) : ViewModel() { var customEmojiFrequency: String? - get() = customEmojiPrefs.customEmojiFrequency + get() = customEmojiPrefs.customEmojiFrequency.value set(value) { - customEmojiPrefs.customEmojiFrequency = value + customEmojiPrefs.setCustomEmojiFrequency(value) } } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt index 70a25a5e2..16c5f5cfb 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateManager.kt @@ -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 -> "" } diff --git a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 92d70fe4e..2f3b9e449 100644 --- a/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/main/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -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 diff --git a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt index 66b2e3b0c..1f3d5c21c 100644 --- a/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/fdroid/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -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 = _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()) diff --git a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt index d47db4035..8c88f99e1 100644 --- a/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt +++ b/feature/map/src/google/kotlin/org/meshtastic/feature/map/MapViewModel.kt @@ -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 = _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 } } diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt index d37715e47..06037e880 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/BaseMapViewModel.kt @@ -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 diff --git a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt index 535c87227..7619a3246 100644 --- a/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt +++ b/feature/map/src/main/kotlin/org/meshtastic/feature/map/node/NodeMapViewModel.kt @@ -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) } diff --git a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt index cbf7a8443..a66a3a255 100644 --- a/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt +++ b/feature/map/src/testGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt @@ -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) diff --git a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt index a767eaee0..a991d1061 100644 --- a/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt +++ b/feature/messaging/src/main/kotlin/org/meshtastic/feature/messaging/MessageViewModel.kt @@ -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 = _showQuickChat private val _showFiltered = MutableStateFlow(false) @@ -109,7 +109,7 @@ constructor( val frequentEmojis: List 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 = 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 } diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt index 53b753da5..16614f012 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/domain/usecase/GetNodeDetailsUseCase.kt @@ -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 diff --git a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt index 7e1002c40..29d948898 100644 --- a/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt +++ b/feature/node/src/main/kotlin/org/meshtastic/feature/node/metrics/MetricsViewModel.kt @@ -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> = 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> = 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" } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt index db8aceff7..6c48316b4 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/SettingsViewModel.kt @@ -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 = _meshLogRetentionDays.asStateFlow() - private val _meshLogLoggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled) + private val _meshLogLoggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled.value) val meshLogLoggingEnabled: StateFlow = _meshLogLoggingEnabled.asStateFlow() fun setMeshLogRetentionDays(days: Int) { diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index c58f34232..deccdc951 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -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 = _retentionDays.asStateFlow() - private val _loggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled) + private val _loggingEnabled = MutableStateFlow(meshLogPrefs.loggingEnabled.value) val loggingEnabled: StateFlow = _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) } } } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt index cc263bfe1..e851b4880 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModel.kt @@ -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 = _filterEnabled.asStateFlow() - private val _filterWords = MutableStateFlow(filterPrefs.filterWords.toList().sorted()) + private val _filterWords = MutableStateFlow(filterPrefs.filterWords.value.toList().sorted()) val filterWords: StateFlow> = _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() } diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt index 54b04c295..839e8d0e0 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModel.kt @@ -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() diff --git a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt index 4451e9f67..47c98eaf8 100644 --- a/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt +++ b/feature/settings/src/main/kotlin/org/meshtastic/feature/settings/radio/component/MQTTConfigItemList.kt @@ -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) } diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt index 7e628b85b..9af1f1c0d 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/SettingsViewModelTest.kt @@ -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) diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt index 101cce4fe..582327179 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt @@ -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) } diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt index 40bb475eb..eae08f319 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/filter/FilterSettingsViewModelTest.kt @@ -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) } diff --git a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index adf6dd9ac..b2067fbf2 100644 --- a/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/test/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -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