Pref fixes (#3175)

This commit is contained in:
Phil Oliver 2025-09-23 15:52:09 -04:00 committed by James Rich
parent c5c433c165
commit a1d9f926cb
7 changed files with 99 additions and 29 deletions

View file

@ -0,0 +1,68 @@
/*
* 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.datastore
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.Dispatchers
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 javax.inject.Inject
import javax.inject.Singleton
internal const val KEY_APP_INTRO_COMPLETED = "app_intro_completed"
internal const val KEY_THEME = "theme"
@Singleton
class UiPreferencesDataSource @Inject constructor(private val dataStore: DataStore<Preferences>) {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val appIntroCompleted: StateFlow<Boolean> = dataStore.prefStateFlow(key = APP_INTRO_COMPLETED, default = false)
// Default value for AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
val theme: StateFlow<Int> = dataStore.prefStateFlow(key = THEME, default = -1)
fun setAppIntroCompleted(completed: Boolean) {
dataStore.setPref(key = APP_INTRO_COMPLETED, value = completed)
}
fun setTheme(value: Int) {
dataStore.setPref(key = THEME, value = value)
}
private fun <T : Any> DataStore<Preferences>.prefStateFlow(key: Preferences.Key<T>, default: T): StateFlow<T> =
data.map { it[key] ?: default }.stateIn(scope = scope, started = SharingStarted.Lazily, initialValue = default)
private fun <T : Any> DataStore<Preferences>.setPref(key: Preferences.Key<T>, value: T) {
scope.launch { edit { it[key] = value } }
}
private companion object {
val APP_INTRO_COMPLETED = booleanPreferencesKey(KEY_APP_INTRO_COMPLETED)
val THEME = intPreferencesKey(KEY_THEME)
}
}

View file

@ -38,6 +38,8 @@ 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_THEME
import org.meshtastic.core.datastore.serializer.ChannelSetSerializer
import org.meshtastic.core.datastore.serializer.LocalConfigSerializer
import org.meshtastic.core.datastore.serializer.ModuleConfigSerializer
@ -53,7 +55,15 @@ object DataStoreModule {
fun providePreferencesDataStore(@ApplicationContext appContext: Context): DataStore<Preferences> =
PreferenceDataStoreFactory.create(
corruptionHandler = ReplaceFileCorruptionHandler(produceNewData = { emptyPreferences() }),
migrations = listOf(SharedPreferencesMigration(appContext, USER_PREFERENCES_NAME)),
migrations =
listOf(
SharedPreferencesMigration(context = appContext, sharedPreferencesName = USER_PREFERENCES_NAME),
SharedPreferencesMigration(
context = appContext,
sharedPreferencesName = "ui-prefs",
keysToMigrate = setOf(KEY_APP_INTRO_COMPLETED, KEY_THEME),
),
),
scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
produceFile = { appContext.preferencesDataStoreFile(USER_PREFERENCES_NAME) },
)

View file

@ -18,12 +18,10 @@
package org.meshtastic.core.prefs.ui
import android.content.SharedPreferences
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import org.meshtastic.core.prefs.PrefDelegate
import org.meshtastic.core.prefs.di.UiSharedPreferences
import java.util.concurrent.ConcurrentHashMap
@ -31,10 +29,6 @@ import javax.inject.Inject
import javax.inject.Singleton
interface UiPrefs {
var theme: Int
val themeFlow: StateFlow<Int>
var appIntroCompleted: Boolean
val appIntroCompletedFlow: StateFlow<Boolean>
var hasShownNotPairedWarning: Boolean
var nodeSortOption: Int
var includeUnknown: Boolean
@ -49,28 +43,15 @@ interface UiPrefs {
fun setShouldProvideNodeLocation(nodeNum: Int, value: Boolean)
}
const val KEY_THEME = "theme"
const val KEY_APP_INTRO_COMPLETED = "app_intro_completed"
@Singleton
class UiPrefsImpl @Inject constructor(@UiSharedPreferences private val prefs: SharedPreferences) : UiPrefs {
override var theme: Int by PrefDelegate(prefs, KEY_THEME, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
private var _themeFlow = MutableStateFlow(theme)
override val themeFlow = _themeFlow.asStateFlow()
override var appIntroCompleted: Boolean by PrefDelegate(prefs, KEY_APP_INTRO_COMPLETED, false)
private var _appIntroCompletedFlow = MutableStateFlow(appIntroCompleted)
override val appIntroCompletedFlow = _appIntroCompletedFlow.asStateFlow()
// 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) {
KEY_THEME -> _themeFlow.update { theme }
KEY_APP_INTRO_COMPLETED -> _appIntroCompletedFlow.update { appIntroCompleted }
// Check if the changed key is one of our node location keys
else ->
provideNodeLocationFlows.keys.forEach { nodeNum ->