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

@ -139,6 +139,8 @@ android {
release {
if (keystoreProperties["storeFile"] != null) {
signingConfig = signingConfigs.named("release").get()
} else {
signingConfig = signingConfigs.getByName("debug")
}
}
}

View file

@ -39,6 +39,7 @@ import androidx.compose.ui.platform.LocalView
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.UIViewModel
@ -48,8 +49,9 @@ import com.geeksville.mesh.ui.common.theme.MODE_DYNAMIC
import com.geeksville.mesh.ui.intro.AppIntroductionScreen
import com.geeksville.mesh.ui.sharing.toSharedContact
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
import org.meshtastic.core.prefs.ui.UiPrefs
import javax.inject.Inject
@AndroidEntryPoint
@ -61,7 +63,7 @@ class MainActivity :
// This is aware of the Activity lifecycle and handles binding to the mesh service.
@Inject internal lateinit var meshServiceClient: MeshServiceClient
@Inject internal lateinit var uiPrefs: UiPrefs
@Inject internal lateinit var uiPreferencesDataSource: UiPreferencesDataSource
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
@ -77,8 +79,11 @@ class MainActivity :
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
if (uiPrefs.appIntroCompleted) {
(application as GeeksvilleApplication).askToRate(this)
lifecycleScope.launch {
val appIntroCompleted = uiPreferencesDataSource.appIntroCompleted.value
if (appIntroCompleted) {
(application as GeeksvilleApplication).askToRate(this@MainActivity)
}
}
}

View file

@ -81,6 +81,7 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.model.DeviceHardware
import org.meshtastic.core.prefs.ui.UiPrefs
import org.meshtastic.core.strings.R
@ -188,11 +189,12 @@ constructor(
private val quickChatActionRepository: QuickChatActionRepository,
firmwareReleaseRepository: FirmwareReleaseRepository,
private val uiPrefs: UiPrefs,
private val uiPreferencesDataSource: UiPreferencesDataSource,
private val meshServiceNotifications: MeshServiceNotifications,
) : ViewModel(),
Logging {
val theme: StateFlow<Int> = uiPrefs.themeFlow
val theme: StateFlow<Int> = uiPreferencesDataSource.theme
private val _lastTraceRouteTime = MutableStateFlow<Long?>(null)
val lastTraceRouteTime: StateFlow<Long?> = _lastTraceRouteTime.asStateFlow()
@ -822,9 +824,9 @@ constructor(
nodeFilterText.value = text
}
val appIntroCompleted: StateFlow<Boolean> = uiPrefs.appIntroCompletedFlow
val appIntroCompleted: StateFlow<Boolean> = uiPreferencesDataSource.appIntroCompleted
fun onAppIntroCompleted() {
uiPrefs.appIntroCompleted = true
uiPreferencesDataSource.setAppIntroCompleted(true)
}
}

View file

@ -47,6 +47,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.meshtastic.core.datastore.UiPreferencesDataSource
import org.meshtastic.core.prefs.ui.UiPrefs
import java.io.BufferedWriter
import java.io.FileNotFoundException
@ -65,6 +66,7 @@ constructor(
private val nodeRepository: NodeRepository,
private val meshLogRepository: MeshLogRepository,
private val uiPrefs: UiPrefs,
private val uiPreferencesDataSource: UiPreferencesDataSource,
) : ViewModel(),
Logging {
val myNodeInfo: StateFlow<MyNodeEntity?> = nodeRepository.myNodeInfo
@ -109,11 +111,11 @@ constructor(
}
fun setTheme(theme: Int) {
uiPrefs.theme = theme
uiPreferencesDataSource.setTheme(theme)
}
fun showAppIntro() {
uiPrefs.appIntroCompleted = false
uiPreferencesDataSource.setAppIntroCompleted(false)
}
fun unlockExcludedModules() {

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