mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: migrate preferences to DataStore and decouple core:domain for KMP (#4731)
This commit is contained in:
parent
87fdaa26ff
commit
b9b68d2779
113 changed files with 1790 additions and 1320 deletions
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue