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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,146 +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.datastore.di
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.core.DataStoreFactory
import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler
import androidx.datastore.dataStoreFile
import androidx.datastore.preferences.SharedPreferencesMigration
import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.emptyPreferences
import androidx.datastore.preferences.preferencesDataStoreFile
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.datastore.KEY_APP_INTRO_COMPLETED
import org.meshtastic.core.datastore.KEY_INCLUDE_UNKNOWN
import org.meshtastic.core.datastore.KEY_NODE_SORT
import org.meshtastic.core.datastore.KEY_ONLY_DIRECT
import org.meshtastic.core.datastore.KEY_ONLY_ONLINE
import org.meshtastic.core.datastore.KEY_SHOW_IGNORED
import org.meshtastic.core.datastore.KEY_THEME
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.LocalStatsSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.LocalConfig
import org.meshtastic.proto.LocalModuleConfig
import org.meshtastic.proto.LocalStats
import javax.inject.Qualifier
import javax.inject.Singleton
private const val USER_PREFERENCES_NAME = "user_preferences"
@Retention(AnnotationRetention.BINARY)
@Qualifier
annotation class DataStoreScope
@InstallIn(SingletonComponent::class)
@Module
object DataStoreModule {
@Provides
@Singleton
@DataStoreScope
fun provideDataStoreScope(): CoroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@Singleton
@Provides
fun providePreferencesDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<Preferences> = PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
migrations =
listOf(
SharedPreferencesMigration(context = appContext, sharedPreferencesName = USER_PREFERENCES_NAME),
SharedPreferencesMigration(
context = appContext,
sharedPreferencesName = "ui-prefs",
keysToMigrate =
setOf(
KEY_APP_INTRO_COMPLETED,
KEY_THEME,
KEY_NODE_SORT,
KEY_INCLUDE_UNKNOWN,
KEY_ONLY_ONLINE,
KEY_ONLY_DIRECT,
KEY_SHOW_IGNORED,
),
),
),
scope = scope,
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
)
@Singleton
@Provides
fun provideLocalConfigDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalConfig> = DataStoreFactory.create(
serializer = LocalConfigSerializer,
produceFile = { appContext.dataStoreFile("local_config.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalConfig() }),
scope = scope,
)
@Singleton
@Provides
fun provideModuleConfigDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalModuleConfig> = DataStoreFactory.create(
serializer = ModuleConfigSerializer,
produceFile = { appContext.dataStoreFile("module_config.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalModuleConfig() }),
scope = scope,
)
@Singleton
@Provides
fun provideChannelSetDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<ChannelSet> = DataStoreFactory.create(
serializer = ChannelSetSerializer,
produceFile = { appContext.dataStoreFile("channel_set.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { ChannelSet() }),
scope = scope,
)
@Singleton
@Provides
fun provideLocalStatsDataStore(
@ApplicationContext appContext: Context,
@DataStoreScope scope: CoroutineScope,
): DataStore<LocalStats> = DataStoreFactory.create(
serializer = LocalStatsSerializer,
produceFile = { appContext.dataStoreFile("local_stats.pb") },
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { LocalStats() }),
scope = scope,
)
}