diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/AnalyticsPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/AnalyticsPrefs.kt new file mode 100644 index 000000000..a0bc2dda9 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/AnalyticsPrefs.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences + +interface AnalyticsPrefs { + var analyticsAllowed: Boolean +} + +class AnalyticsPrefsImpl(prefs: SharedPreferences) : AnalyticsPrefs { + override var analyticsAllowed: Boolean by PrefDelegate(prefs, "allowed", true) +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/CustomEmojiPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/CustomEmojiPrefs.kt new file mode 100644 index 000000000..eb9f51eab --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/CustomEmojiPrefs.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences + +interface CustomEmojiPrefs { + var customEmojiFrequency: String? +} + +class CustomEmojiPrefsImpl(prefs: SharedPreferences) : CustomEmojiPrefs { + override var customEmojiFrequency: String? by NullableStringPrefDelegate(prefs, "pref_key_custom_emoji_freq", null) +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/MapConsentPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/MapConsentPrefs.kt new file mode 100644 index 000000000..b265f8ff9 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/MapConsentPrefs.kt @@ -0,0 +1,35 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences +import androidx.core.content.edit + +interface MapConsentPrefs { + fun shouldReportLocation(nodeNum: Int?): Boolean + + fun setShouldReportLocation(nodeNum: Int?, value: Boolean) +} + +class MapConsentPrefsImpl(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) } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/MapTileProviderPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/MapTileProviderPrefs.kt new file mode 100644 index 000000000..a97f5a1f6 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/MapTileProviderPrefs.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences + +interface MapTileProviderPrefs { + var customTileProviders: String? +} + +class MapTileProviderPrefsImpl(prefs: SharedPreferences) : MapTileProviderPrefs { + override var customTileProviders: String? by NullableStringPrefDelegate(prefs, "custom_tile_providers", null) +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/MeshPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/MeshPrefs.kt new file mode 100644 index 000000000..335000f1a --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/MeshPrefs.kt @@ -0,0 +1,42 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences +import androidx.core.content.edit + +interface MeshPrefs { + var deviceAddress: String? + + fun shouldProvideNodeLocation(nodeNum: Int?): Boolean + + fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) +} + +class MeshPrefsImpl(private val prefs: SharedPreferences) : MeshPrefs { + override var deviceAddress: String? by NullableStringPrefDelegate(prefs, "device_address", null) + + override fun shouldProvideNodeLocation(nodeNum: Int?): Boolean = + prefs.getBoolean(provideLocationKey(nodeNum), false) + + override fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) { + prefs.edit { putBoolean(provideLocationKey(nodeNum), value) } + } + + private fun provideLocationKey(nodeNum: Int?) = "provide-location-$nodeNum" +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/NullableStringPrefDelegate.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/NullableStringPrefDelegate.kt new file mode 100644 index 000000000..9d1755b0b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/NullableStringPrefDelegate.kt @@ -0,0 +1,48 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.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. + */ +class NullableStringPrefDelegate( + private val prefs: SharedPreferences, + private val key: String, + private val defaultValue: String?, +) : ReadWriteProperty { + + 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) + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/PrefDelegate.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/PrefDelegate.kt new file mode 100644 index 000000000..22948a914 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/PrefDelegate.kt @@ -0,0 +1,58 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.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. + */ +class PrefDelegate(private val prefs: SharedPreferences, private val key: String, private val defaultValue: T) : + ReadWriteProperty { + + @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") + } + } + } +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/PrefsModule.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/PrefsModule.kt new file mode 100644 index 000000000..8fcd0d62b --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/PrefsModule.kt @@ -0,0 +1,143 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.Context +import android.content.SharedPreferences +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton + +// These pref store qualifiers are private to prevent prefs stores from being injected directly. +// Consuming code should always inject one of the prefs repositories. + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class AnalyticsSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class CustomEmojiSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class MapConsentSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class MapTileProviderSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class MeshSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class RadioSharedPreferences + +@Qualifier +@Retention(AnnotationRetention.BINARY) +private annotation class UiSharedPreferences + +@Suppress("TooManyFunctions") +@InstallIn(SingletonComponent::class) +@Module +object PrefsModule { + + @Provides + @Singleton + @AnalyticsSharedPreferences + fun provideAnalyticsSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + @CustomEmojiSharedPreferences + fun provideCustomEmojiSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("org.geeksville.emoji.prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + @MapConsentSharedPreferences + fun provideMapConsentSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("map_consent_preferences", Context.MODE_PRIVATE) + + @Provides + @Singleton + @MapTileProviderSharedPreferences + fun provideMapTileProviderSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("map_tile_provider_prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + @MeshSharedPreferences + fun provideMeshSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("mesh-prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + @RadioSharedPreferences + fun provideRadioSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("radio-prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + @UiSharedPreferences + fun provideUiSharedPreferences(@ApplicationContext context: Context): SharedPreferences = + context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE) + + @Provides + @Singleton + fun provideAnalyticsPrefs(@AnalyticsSharedPreferences sharedPreferences: SharedPreferences): AnalyticsPrefs = + AnalyticsPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideCustomEmojiPrefs(@CustomEmojiSharedPreferences sharedPreferences: SharedPreferences): CustomEmojiPrefs = + CustomEmojiPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideMapConsentPrefs(@MapConsentSharedPreferences sharedPreferences: SharedPreferences): MapConsentPrefs = + MapConsentPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideMapTileProviderPrefs( + @MapTileProviderSharedPreferences sharedPreferences: SharedPreferences, + ): MapTileProviderPrefs = MapTileProviderPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideMeshPrefs(@MeshSharedPreferences sharedPreferences: SharedPreferences): MeshPrefs = + MeshPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideRadioPrefs(@RadioSharedPreferences sharedPreferences: SharedPreferences): RadioPrefs = + RadioPrefsImpl(sharedPreferences) + + @Provides + @Singleton + fun provideUiPrefs(@UiSharedPreferences sharedPreferences: SharedPreferences): UiPrefs = + UiPrefsImpl(sharedPreferences) +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/RadioPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/RadioPrefs.kt new file mode 100644 index 000000000..f53beb804 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/RadioPrefs.kt @@ -0,0 +1,28 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences + +interface RadioPrefs { + var devAddr: String? +} + +class RadioPrefsImpl(prefs: SharedPreferences) : RadioPrefs { + override var devAddr: String? by NullableStringPrefDelegate(prefs, "devAddr2", null) +} diff --git a/app/src/main/java/com/geeksville/mesh/android/prefs/UiPrefs.kt b/app/src/main/java/com/geeksville/mesh/android/prefs/UiPrefs.kt new file mode 100644 index 000000000..52d4328b7 --- /dev/null +++ b/app/src/main/java/com/geeksville/mesh/android/prefs/UiPrefs.kt @@ -0,0 +1,78 @@ +/* + * 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 . + */ + +package com.geeksville.mesh.android.prefs + +import android.content.SharedPreferences +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.edit +import com.geeksville.mesh.model.NodeSortOption +import com.geeksville.mesh.util.LanguageUtils + +interface UiPrefs { + var lang: String + var theme: Int + var appIntroCompleted: Boolean + var hasShownNotPairedWarning: Boolean + var nodeSortOption: Int + var includeUnknown: Boolean + var showDetails: Boolean + var onlyOnline: Boolean + var onlyDirect: Boolean + var showIgnored: Boolean + var showQuickChat: Boolean + + // region Map prefs + + var showOnlyFavorites: Boolean + var showWaypointsOnMap: Boolean + var showPrecisionCircleOnMap: Boolean + var mapStyle: Int + + // endregion + + fun shouldProvideNodeLocation(nodeNum: Int?): Boolean + + fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) +} + +class UiPrefsImpl(private val prefs: SharedPreferences) : UiPrefs { + override var lang: String by PrefDelegate(prefs, "lang", LanguageUtils.SYSTEM_DEFAULT) + override var theme: Int by PrefDelegate(prefs, "theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + override var appIntroCompleted: Boolean by PrefDelegate(prefs, "app_intro_completed", false) + override var hasShownNotPairedWarning: Boolean by PrefDelegate(prefs, "has_shown_not_paired_warning", false) + override var nodeSortOption: Int by PrefDelegate(prefs, "node-sort-option", NodeSortOption.VIA_FAVORITE.ordinal) + override var includeUnknown: Boolean by PrefDelegate(prefs, "include-unknown", false) + override var showDetails: Boolean by PrefDelegate(prefs, "show-details", false) + override var onlyOnline: Boolean by PrefDelegate(prefs, "only-online", false) + override var onlyDirect: Boolean by PrefDelegate(prefs, "only-direct", false) + override var showIgnored: Boolean by PrefDelegate(prefs, "show-ignored", false) + override var showQuickChat: Boolean by PrefDelegate(prefs, "show-quick-chat", false) + override var showOnlyFavorites: Boolean by PrefDelegate(prefs, "only-favorites", false) + override var showWaypointsOnMap: Boolean by PrefDelegate(prefs, "show-waypoints-on-map", true) + override var showPrecisionCircleOnMap: Boolean by PrefDelegate(prefs, "show-precision-circle-on-map", true) + override var mapStyle: Int by PrefDelegate(prefs, "map_style_id", 0) + + override fun shouldProvideNodeLocation(nodeNum: Int?): Boolean = + prefs.getBoolean(provideLocationKey(nodeNum), false) + + override fun setShouldProvideNodeLocation(nodeNum: Int?, value: Boolean) { + prefs.edit { putBoolean(provideLocationKey(nodeNum), value) } + } + + private fun provideLocationKey(nodeNum: Int?) = "provide-location-$nodeNum" +}