mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-03-29 11:13:41 -04:00
refactor: migrate preferences to DataStore and decouple core:domain for KMP (#4731)
This commit is contained in:
@@ -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"))
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> = _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<String?> = _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
|
||||
|
||||
@@ -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<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning)
|
||||
private val _hasShownNotPairedWarning = MutableStateFlow(uiPrefs.hasShownNotPairedWarning.value)
|
||||
val hasShownNotPairedWarning: StateFlow<Boolean> = _hasShownNotPairedWarning.asStateFlow()
|
||||
|
||||
fun suppressNoPairedWarning() {
|
||||
_hasShownNotPairedWarning.value = true
|
||||
uiPrefs.hasShownNotPairedWarning = true
|
||||
uiPrefs.setHasShownNotPairedWarning(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -27,6 +27,7 @@ plugins {
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.prefs)
|
||||
implementation(projects.core.repository)
|
||||
|
||||
implementation(libs.androidx.compose.runtime)
|
||||
implementation(libs.androidx.lifecycle.process)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.repository
|
||||
package org.meshtastic.core.common.database
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<List<CustomTileProviderConfig>>(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" }
|
||||
}
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<LocalConfig> = 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<LocalModuleConfig> = 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<ChannelSet> = 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<LocalStats> = 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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<List<MeshLog>> =
|
||||
override fun getAllLogs(maxItem: Int): Flow<List<MeshLog>> =
|
||||
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<List<MeshLog>> =
|
||||
override fun getAllLogsInReceiveOrder(maxItem: Int): Flow<List<MeshLog>> =
|
||||
dbManager.currentDb.flatMapLatest { it.meshLogDao().getAllLogsInReceiveOrder(maxItem) }.flowOn(dispatchers.io)
|
||||
|
||||
/** Retrieves all [MeshLog]s in the database without any limit. */
|
||||
fun getAllLogsUnbounded(): Flow<List<MeshLog>> = getAllLogs(Int.MAX_VALUE)
|
||||
override fun getAllLogsUnbounded(): Flow<List<MeshLog>> = getAllLogs(Int.MAX_VALUE)
|
||||
|
||||
/** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */
|
||||
fun getLogsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(nodeNum, portNum, MAX_MESH_PACKETS) }
|
||||
override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshLog>> = 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<List<MeshPacket>> =
|
||||
override fun getMeshPacketsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshPacket>> =
|
||||
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<List<Telemetry>> = effectiveLogId(nodeNum)
|
||||
override fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>> = 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<List<MeshLog>> = dbManager.currentDb
|
||||
.flatMapLatest { it.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, portNum.value, MAX_MESH_PACKETS) }
|
||||
override fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow<List<MeshLog>> = 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<MyNodeInfo?> = dbManager.currentDb
|
||||
.flatMapLatest { db -> db.meshLogDao().getLogsFrom(MeshLog.NODE_NUM_LOCAL, 0, MAX_MESH_PACKETS) }
|
||||
override fun getMyNodeInfo(): Flow<MyNodeInfo?> = 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<Boolean>
|
||||
private lateinit var filterWordsFlow: MutableStateFlow<Set<String>>
|
||||
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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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?
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,38 +14,29 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<LibraryExtension> { 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) }
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.datastore
|
||||
|
||||
import androidx.datastore.core.DataStore
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Preferences>) {
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.meshtastic.core.datastore.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
@@ -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<ChannelSet> {
|
||||
object ChannelSetSerializer : OkioSerializer<ChannelSet> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<LocalConfig> {
|
||||
object LocalConfigSerializer : OkioSerializer<LocalConfig> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<LocalStats> {
|
||||
object LocalStatsSerializer : OkioSerializer<LocalStats> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<LocalModuleConfig> {
|
||||
object ModuleConfigSerializer : OkioSerializer<LocalModuleConfig> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ class SendMessageUseCaseTest {
|
||||
val ourNode = mockk<Node>(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<Capabilities>().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<Capabilities>().canSendVerifiedContacts } returns true
|
||||
|
||||
// Act
|
||||
@@ -158,7 +158,7 @@ class SendMessageUseCaseTest {
|
||||
// Arrange
|
||||
val ourNode = mockk<Node>(relaxed = true)
|
||||
every { nodeRepository.ourNodeInfo } returns MutableStateFlow(ourNode)
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled } returns true
|
||||
every { homoglyphEncodingPrefs.homoglyphEncodingEnabled.value } returns true
|
||||
|
||||
val originalText = "\u0410pple" // Cyrillic A
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<Node>(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<Node>(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<org.meshtastic.core.model.DeviceHardware> { every { requiresDfu } returns true }
|
||||
coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||
@@ -111,7 +111,7 @@ class IsOtaCapableUseCaseTest {
|
||||
val node = mockk<Node>(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<org.meshtastic.core.model.DeviceHardware> { every { requiresDfu } returns false }
|
||||
coEvery { deviceHardwareRepository.getDeviceHardwareByModel(any()) } returns Result.success(hw)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,13 @@ configure<LibraryExtension> { 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)
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(context, "google_maps_prefs")),
|
||||
scope = scope,
|
||||
produceFile = { context.preferencesDataStoreFile("google_maps_ds") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String>
|
||||
var cameraTargetLat: Double
|
||||
var cameraTargetLng: Double
|
||||
var cameraZoom: Float
|
||||
var cameraTilt: Float
|
||||
var cameraBearing: Float
|
||||
var networkMapLayers: Set<String>
|
||||
val selectedGoogleMapType: StateFlow<String?>
|
||||
|
||||
fun setSelectedGoogleMapType(value: String?)
|
||||
|
||||
val selectedCustomTileUrl: StateFlow<String?>
|
||||
|
||||
fun setSelectedCustomTileUrl(value: String?)
|
||||
|
||||
val hiddenLayerUrls: StateFlow<Set<String>>
|
||||
|
||||
fun setHiddenLayerUrls(value: Set<String>)
|
||||
|
||||
val cameraTargetLat: StateFlow<Double>
|
||||
|
||||
fun setCameraTargetLat(value: Double)
|
||||
|
||||
val cameraTargetLng: StateFlow<Double>
|
||||
|
||||
fun setCameraTargetLng(value: Double)
|
||||
|
||||
val cameraZoom: StateFlow<Float>
|
||||
|
||||
fun setCameraZoom(value: Float)
|
||||
|
||||
val cameraTilt: StateFlow<Float>
|
||||
|
||||
fun setCameraTilt(value: Float)
|
||||
|
||||
val cameraBearing: StateFlow<Float>
|
||||
|
||||
fun setCameraBearing(value: Float)
|
||||
|
||||
val networkMapLayers: StateFlow<Set<String>>
|
||||
|
||||
fun setNetworkMapLayers(value: Set<String>)
|
||||
}
|
||||
|
||||
@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<String> 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<String> by StringSetPrefDelegate(prefs, "network_map_layers", emptySet())
|
||||
class GoogleMapsPrefsImpl
|
||||
@Inject
|
||||
constructor(
|
||||
@GoogleMapsDataStore private val dataStore: DataStore<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : GoogleMapsPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val selectedGoogleMapType: StateFlow<String?> =
|
||||
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<String?> =
|
||||
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<Set<String>> =
|
||||
dataStore.data
|
||||
.map { it[KEY_HIDDEN_LAYER_URLS_PREF] ?: emptySet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
override fun setHiddenLayerUrls(value: Set<String>) {
|
||||
scope.launch { dataStore.edit { it[KEY_HIDDEN_LAYER_URLS_PREF] = value } }
|
||||
}
|
||||
|
||||
override val cameraTargetLat: StateFlow<Double> =
|
||||
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<Double> =
|
||||
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<Float> =
|
||||
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<Float> =
|
||||
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<Float> =
|
||||
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<Set<String>> =
|
||||
dataStore.data
|
||||
.map { it[KEY_NETWORK_MAP_LAYERS_PREF] ?: emptySet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
override fun setNetworkMapLayers(value: Set<String>) {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Any?, Double> {
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Any?, Float> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Any?, String?> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<T>(
|
||||
private val prefs: SharedPreferences,
|
||||
private val key: String,
|
||||
private val defaultValue: T,
|
||||
) : ReadWriteProperty<Any?, T> {
|
||||
|
||||
@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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<String>,
|
||||
) : ReadWriteProperty<Any?, Set<String>> {
|
||||
override fun getValue(thisRef: Any?, property: KProperty<*>): Set<String> =
|
||||
prefs.getStringSet(key, defaultValue) ?: emptySet()
|
||||
|
||||
override fun setValue(thisRef: Any?, property: KProperty<*>, value: Set<String>) =
|
||||
prefs.edit { putStringSet(key, value) }
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Boolean>
|
||||
|
||||
/** 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<Boolean> = 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) }
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
@AppDataStore private val appDataStore: DataStore<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : AnalyticsPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val analyticsAllowed: StateFlow<Boolean> =
|
||||
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<String> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
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<Preferences> =
|
||||
PreferenceDataStoreFactory.create(
|
||||
migrations = listOf(SharedPreferencesMigration(context, "filter-prefs")),
|
||||
scope = scope,
|
||||
produceFile = { context.preferencesDataStoreFile("filter_ds") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : CustomEmojiPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val customEmojiFrequency: StateFlow<String?> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<String>
|
||||
|
||||
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<String> by StringSetPrefDelegate(prefs, FilterPrefs.KEY_FILTER_WORDS, emptySet())
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : FilterPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val filterEnabled: StateFlow<Boolean> =
|
||||
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<Set<String>> =
|
||||
dataStore.data
|
||||
.map { it[KEY_FILTER_WORDS_PREF] ?: emptySet() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, emptySet())
|
||||
|
||||
override fun setFilterWords(words: Set<String>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Boolean>
|
||||
|
||||
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<Boolean> = 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) }
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : HomoglyphPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val homoglyphEncodingEnabled: StateFlow<Boolean> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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) }
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MapConsentPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
private val consentFlows = ConcurrentHashMap<Int?, StateFlow<Boolean>>()
|
||||
|
||||
override fun shouldReportLocation(nodeNum: Int?): StateFlow<Boolean> = 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 } }
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MapPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val mapStyle: StateFlow<Int> =
|
||||
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<Boolean> =
|
||||
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<Boolean> =
|
||||
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<Boolean> =
|
||||
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<Long> =
|
||||
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<Long> =
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MapTileProviderPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val customTileProviders: StateFlow<String?> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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"
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MeshPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
private val locationFlows = ConcurrentHashMap<Int?, StateFlow<Boolean>>()
|
||||
private val storeForwardFlows = ConcurrentHashMap<String?, StateFlow<Int>>()
|
||||
|
||||
override val deviceAddress: StateFlow<String?> =
|
||||
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<Boolean> = 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<Int> = 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"
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : MeshLogPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val retentionDays: StateFlow<Int> =
|
||||
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<Boolean> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
dispatchers: CoroutineDispatchers,
|
||||
) : RadioPrefs {
|
||||
private val scope = CoroutineScope(SupervisorJob() + dispatchers.default)
|
||||
|
||||
override val devAddr: StateFlow<String?> =
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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<Boolean>
|
||||
|
||||
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<Int, MutableStateFlow<Boolean>>()
|
||||
|
||||
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<Boolean> = 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"
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<Preferences>,
|
||||
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<Int, StateFlow<Boolean>>()
|
||||
|
||||
override val hasShownNotPairedWarning: StateFlow<Boolean> =
|
||||
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<Boolean> =
|
||||
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<Boolean> =
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -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<Preferences>
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.repository
|
||||
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/** Reactive interface for analytics-related preferences. */
|
||||
interface AnalyticsPrefs {
|
||||
val analyticsAllowed: StateFlow<Boolean>
|
||||
|
||||
fun setAnalyticsAllowed(allowed: Boolean)
|
||||
|
||||
val installId: StateFlow<String>
|
||||
}
|
||||
|
||||
/** Reactive interface for homoglyph encoding preferences. */
|
||||
interface HomoglyphPrefs {
|
||||
val homoglyphEncodingEnabled: StateFlow<Boolean>
|
||||
|
||||
fun setHomoglyphEncodingEnabled(enabled: Boolean)
|
||||
}
|
||||
|
||||
/** Reactive interface for message filtering preferences. */
|
||||
interface FilterPrefs {
|
||||
val filterEnabled: StateFlow<Boolean>
|
||||
|
||||
fun setFilterEnabled(enabled: Boolean)
|
||||
|
||||
val filterWords: StateFlow<Set<String>>
|
||||
|
||||
fun setFilterWords(words: Set<String>)
|
||||
}
|
||||
|
||||
/** Reactive interface for mesh log preferences. */
|
||||
interface MeshLogPrefs {
|
||||
val retentionDays: StateFlow<Int>
|
||||
|
||||
fun setRetentionDays(days: Int)
|
||||
|
||||
val loggingEnabled: StateFlow<Boolean>
|
||||
|
||||
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<String?>
|
||||
|
||||
fun setCustomEmojiFrequency(frequency: String?)
|
||||
}
|
||||
|
||||
/** Reactive interface for general UI preferences. */
|
||||
interface UiPrefs {
|
||||
val hasShownNotPairedWarning: StateFlow<Boolean>
|
||||
|
||||
fun setHasShownNotPairedWarning(shown: Boolean)
|
||||
|
||||
val showQuickChat: StateFlow<Boolean>
|
||||
|
||||
fun setShowQuickChat(show: Boolean)
|
||||
|
||||
fun shouldProvideNodeLocation(nodeNum: Int): StateFlow<Boolean>
|
||||
|
||||
fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean)
|
||||
}
|
||||
|
||||
/** Reactive interface for general map preferences. */
|
||||
interface MapPrefs {
|
||||
val mapStyle: StateFlow<Int>
|
||||
|
||||
fun setMapStyle(style: Int)
|
||||
|
||||
val showOnlyFavorites: StateFlow<Boolean>
|
||||
|
||||
fun setShowOnlyFavorites(show: Boolean)
|
||||
|
||||
val showWaypointsOnMap: StateFlow<Boolean>
|
||||
|
||||
fun setShowWaypointsOnMap(show: Boolean)
|
||||
|
||||
val showPrecisionCircleOnMap: StateFlow<Boolean>
|
||||
|
||||
fun setShowPrecisionCircleOnMap(show: Boolean)
|
||||
|
||||
val lastHeardFilter: StateFlow<Long>
|
||||
|
||||
fun setLastHeardFilter(seconds: Long)
|
||||
|
||||
val lastHeardTrackFilter: StateFlow<Long>
|
||||
|
||||
fun setLastHeardTrackFilter(seconds: Long)
|
||||
}
|
||||
|
||||
/** Reactive interface for map consent. */
|
||||
interface MapConsentPrefs {
|
||||
fun shouldReportLocation(nodeNum: Int?): StateFlow<Boolean>
|
||||
|
||||
fun setShouldReportLocation(nodeNum: Int?, report: Boolean)
|
||||
}
|
||||
|
||||
/** Reactive interface for map tile provider settings. */
|
||||
interface MapTileProviderPrefs {
|
||||
val customTileProviders: StateFlow<String?>
|
||||
|
||||
fun setCustomTileProviders(providers: String?)
|
||||
}
|
||||
|
||||
/** Reactive interface for radio settings. */
|
||||
interface RadioPrefs {
|
||||
val devAddr: StateFlow<String?>
|
||||
|
||||
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<String?>
|
||||
|
||||
fun setDeviceAddress(address: String?)
|
||||
|
||||
fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow<Boolean>
|
||||
|
||||
fun setShouldProvideNodeLocation(nodeNum: Int?, provide: Boolean)
|
||||
|
||||
fun getStoreForwardLastRequest(address: String?): StateFlow<Int>
|
||||
|
||||
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
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.repository
|
||||
|
||||
interface HomoglyphPrefs {
|
||||
val homoglyphEncodingEnabled: Boolean
|
||||
}
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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<List<MeshLog>>
|
||||
|
||||
/** Retrieves all [MeshLog]s in the database in the order they were received. */
|
||||
fun getAllLogsInReceiveOrder(maxItem: Int = DEFAULT_MAX_LOGS): Flow<List<MeshLog>>
|
||||
|
||||
/** Retrieves all [MeshLog]s in the database without any limit. */
|
||||
fun getAllLogsUnbounded(): Flow<List<MeshLog>>
|
||||
|
||||
/** Retrieves all [MeshLog]s associated with a specific [nodeNum] and [portNum]. */
|
||||
fun getLogsFrom(nodeNum: Int, portNum: Int): Flow<List<MeshLog>>
|
||||
|
||||
/** Retrieves all [MeshLog]s containing [MeshPacket]s for a specific [nodeNum]. */
|
||||
fun getMeshPacketsFrom(nodeNum: Int, portNum: Int = -1): Flow<List<MeshPacket>>
|
||||
|
||||
/** Retrieves telemetry history for a specific node, automatically handling local node redirection. */
|
||||
fun getTelemetryFrom(nodeNum: Int): Flow<List<Telemetry>>
|
||||
|
||||
/**
|
||||
* 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<List<MeshLog>>
|
||||
|
||||
/** Returns the cached [MyNodeInfo] from the system logs. */
|
||||
fun getMyNodeInfo(): Flow<MyNodeInfo?>
|
||||
|
||||
/** 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
|
||||
}
|
||||
}
|
||||
@@ -89,7 +89,7 @@ class SendMessageUseCase(
|
||||
|
||||
// Apply homoglyph encoding
|
||||
val finalMessageText =
|
||||
if (homoglyphEncodingPrefs.homoglyphEncodingEnabled) {
|
||||
if (homoglyphEncodingPrefs.homoglyphEncodingEnabled.value) {
|
||||
HomoglyphCharacterStringTransformer.optimizeUtf8StringWithHomoglyphs(text)
|
||||
} else {
|
||||
text
|
||||
|
||||
@@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -> ""
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,7 +26,7 @@ import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
@@ -52,9 +52,9 @@ constructor(
|
||||
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
|
||||
|
||||
var mapStyleId: Int
|
||||
get() = mapPrefs.mapStyle
|
||||
get() = mapPrefs.mapStyle.value
|
||||
set(value) {
|
||||
mapPrefs.mapStyle = value
|
||||
mapPrefs.setMapStyle(value)
|
||||
}
|
||||
|
||||
val localConfig = radioConfigRepository.localConfigFlow.stateInWhileSubscribed(initialValue = LocalConfig())
|
||||
|
||||
@@ -49,7 +49,7 @@ import org.meshtastic.core.datastore.UiPreferencesDataSource
|
||||
import org.meshtastic.core.model.RadioController
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.prefs.map.GoogleMapsPrefs
|
||||
import org.meshtastic.core.prefs.map.MapPrefs
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.core.repository.PacketRepository
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
@@ -96,9 +96,9 @@ constructor(
|
||||
val selectedWaypointId: StateFlow<Int?> = _selectedWaypointId.asStateFlow()
|
||||
|
||||
private val targetLatLng =
|
||||
googleMapsPrefs.cameraTargetLat
|
||||
googleMapsPrefs.cameraTargetLat.value
|
||||
.takeIf { it != 0.0 }
|
||||
?.let { lat -> googleMapsPrefs.cameraTargetLng.takeIf { it != 0.0 }?.let { lng -> LatLng(lat, lng) } }
|
||||
?.let { lat -> googleMapsPrefs.cameraTargetLng.value.takeIf { it != 0.0 }?.let { lng -> LatLng(lat, lng) } }
|
||||
?: ourNodeInfo.value?.position?.toLatLng()
|
||||
?: LatLng(0.0, 0.0)
|
||||
|
||||
@@ -107,9 +107,9 @@ constructor(
|
||||
position =
|
||||
CameraPosition(
|
||||
targetLatLng,
|
||||
googleMapsPrefs.cameraZoom,
|
||||
googleMapsPrefs.cameraTilt,
|
||||
googleMapsPrefs.cameraBearing,
|
||||
googleMapsPrefs.cameraZoom.value,
|
||||
googleMapsPrefs.cameraTilt.value,
|
||||
googleMapsPrefs.cameraBearing.value,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -222,7 +222,7 @@ constructor(
|
||||
) {
|
||||
_selectedCustomTileProviderUrl.value = null
|
||||
// Also clear from prefs
|
||||
googleMapsPrefs.selectedCustomTileUrl = null
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(null)
|
||||
}
|
||||
|
||||
if (configToRemove.localUri != null) {
|
||||
@@ -238,28 +238,28 @@ constructor(
|
||||
if (!config.isLocal && !isValidTileUrlTemplate(config.urlTemplate)) {
|
||||
Logger.withTag("MapViewModel").w("Attempted to select invalid URL template: ${config.urlTemplate}")
|
||||
_selectedCustomTileProviderUrl.value = null
|
||||
googleMapsPrefs.selectedCustomTileUrl = null
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(null)
|
||||
return
|
||||
}
|
||||
// Use localUri if present, otherwise urlTemplate
|
||||
val selectedUrl = config.localUri ?: config.urlTemplate
|
||||
_selectedCustomTileProviderUrl.value = selectedUrl
|
||||
_selectedGoogleMapType.value = MapType.NONE
|
||||
googleMapsPrefs.selectedCustomTileUrl = selectedUrl
|
||||
googleMapsPrefs.selectedGoogleMapType = null
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(selectedUrl)
|
||||
googleMapsPrefs.setSelectedGoogleMapType(null)
|
||||
} else {
|
||||
_selectedCustomTileProviderUrl.value = null
|
||||
_selectedGoogleMapType.value = MapType.NORMAL
|
||||
googleMapsPrefs.selectedCustomTileUrl = null
|
||||
googleMapsPrefs.selectedGoogleMapType = MapType.NORMAL.name
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(null)
|
||||
googleMapsPrefs.setSelectedGoogleMapType(MapType.NORMAL.name)
|
||||
}
|
||||
}
|
||||
|
||||
fun setSelectedGoogleMapType(mapType: MapType) {
|
||||
_selectedGoogleMapType.value = mapType
|
||||
_selectedCustomTileProviderUrl.value = null // Clear custom selection
|
||||
googleMapsPrefs.selectedGoogleMapType = mapType.name
|
||||
googleMapsPrefs.selectedCustomTileUrl = null
|
||||
googleMapsPrefs.setSelectedGoogleMapType(mapType.name)
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(null)
|
||||
}
|
||||
|
||||
private var currentTileProvider: TileProvider? = null
|
||||
@@ -354,16 +354,16 @@ constructor(
|
||||
|
||||
fun saveCameraPosition(cameraPosition: CameraPosition) {
|
||||
viewModelScope.launch {
|
||||
googleMapsPrefs.cameraTargetLat = cameraPosition.target.latitude
|
||||
googleMapsPrefs.cameraTargetLng = cameraPosition.target.longitude
|
||||
googleMapsPrefs.cameraZoom = cameraPosition.zoom
|
||||
googleMapsPrefs.cameraTilt = cameraPosition.tilt
|
||||
googleMapsPrefs.cameraBearing = cameraPosition.bearing
|
||||
googleMapsPrefs.setCameraTargetLat(cameraPosition.target.latitude)
|
||||
googleMapsPrefs.setCameraTargetLng(cameraPosition.target.longitude)
|
||||
googleMapsPrefs.setCameraZoom(cameraPosition.zoom)
|
||||
googleMapsPrefs.setCameraTilt(cameraPosition.tilt)
|
||||
googleMapsPrefs.setCameraBearing(cameraPosition.bearing)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadPersistedMapType() {
|
||||
val savedCustomUrl = googleMapsPrefs.selectedCustomTileUrl
|
||||
val savedCustomUrl = googleMapsPrefs.selectedCustomTileUrl.value
|
||||
if (savedCustomUrl != null) {
|
||||
// Check if this custom provider still exists
|
||||
if (
|
||||
@@ -375,18 +375,18 @@ constructor(
|
||||
MapType.NONE // MapType.NONE to hide google basemap when using custom provider
|
||||
} else {
|
||||
// The saved custom URL is no longer valid or doesn't exist, remove preference
|
||||
googleMapsPrefs.selectedCustomTileUrl = null
|
||||
googleMapsPrefs.setSelectedCustomTileUrl(null)
|
||||
// Fallback to default Google Map type
|
||||
_selectedGoogleMapType.value = MapType.NORMAL
|
||||
}
|
||||
} else {
|
||||
val savedGoogleMapTypeName = googleMapsPrefs.selectedGoogleMapType
|
||||
val savedGoogleMapTypeName = googleMapsPrefs.selectedGoogleMapType.value
|
||||
try {
|
||||
_selectedGoogleMapType.value = MapType.valueOf(savedGoogleMapTypeName ?: MapType.NORMAL.name)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Logger.e(e) { "Invalid saved Google Map type: $savedGoogleMapTypeName" }
|
||||
_selectedGoogleMapType.value = MapType.NORMAL // Fallback in case of invalid stored name
|
||||
googleMapsPrefs.selectedGoogleMapType = null
|
||||
googleMapsPrefs.setSelectedGoogleMapType(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,7 @@ constructor(
|
||||
val persistedLayerFiles = layersDir.listFiles()
|
||||
|
||||
if (persistedLayerFiles != null) {
|
||||
val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls
|
||||
val hiddenLayerUrls = googleMapsPrefs.hiddenLayerUrls.value
|
||||
val loadedItems =
|
||||
persistedLayerFiles.mapNotNull { file ->
|
||||
if (file.isFile) {
|
||||
@@ -429,7 +429,7 @@ constructor(
|
||||
}
|
||||
|
||||
val networkItems =
|
||||
googleMapsPrefs.networkMapLayers.mapNotNull { networkString ->
|
||||
googleMapsPrefs.networkMapLayers.value.mapNotNull { networkString ->
|
||||
try {
|
||||
val parts = networkString.split("|:|")
|
||||
if (parts.size == 3) {
|
||||
@@ -532,7 +532,7 @@ constructor(
|
||||
_mapLayers.value = _mapLayers.value + newItem
|
||||
|
||||
val networkLayerString = "${newItem.id}|:|${newItem.name}|:|${newItem.uri}"
|
||||
googleMapsPrefs.networkMapLayers = googleMapsPrefs.networkMapLayers + networkLayerString
|
||||
googleMapsPrefs.setNetworkMapLayers(googleMapsPrefs.networkMapLayers.value + networkLayerString)
|
||||
} catch (e: Exception) {
|
||||
_errorFlow.emit("Invalid URL.")
|
||||
}
|
||||
@@ -572,9 +572,9 @@ constructor(
|
||||
|
||||
toggledLayer?.let {
|
||||
if (it.isVisible) {
|
||||
googleMapsPrefs.hiddenLayerUrls -= it.uri.toString()
|
||||
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value - it.uri.toString())
|
||||
} else {
|
||||
googleMapsPrefs.hiddenLayerUrls += it.uri.toString()
|
||||
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value + it.uri.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -584,12 +584,13 @@ constructor(
|
||||
val layerToRemove = _mapLayers.value.find { it.id == layerId }
|
||||
layerToRemove?.uri?.let { uri ->
|
||||
if (layerToRemove.isNetwork) {
|
||||
googleMapsPrefs.networkMapLayers =
|
||||
googleMapsPrefs.networkMapLayers.filterNot { it.startsWith("$layerId|:|") }.toSet()
|
||||
googleMapsPrefs.setNetworkMapLayers(
|
||||
googleMapsPrefs.networkMapLayers.value.filterNot { it.startsWith("$layerId|:|") }.toSet(),
|
||||
)
|
||||
} else {
|
||||
deleteFileToInternalStorage(uri)
|
||||
}
|
||||
googleMapsPrefs.hiddenLayerUrls -= uri.toString()
|
||||
googleMapsPrefs.setHiddenLayerUrls(googleMapsPrefs.hiddenLayerUrls.value - uri.toString())
|
||||
}
|
||||
_mapLayers.value = _mapLayers.value.filterNot { it.id == layerId }
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user