Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:56:56 +01:00
parent 75dc487a7a
commit 39c29d175b
6317 changed files with 388324 additions and 2 deletions

View file

@ -0,0 +1,10 @@
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.preference"
buildFeatures {
buildConfig = true
}
}

View file

@ -0,0 +1,5 @@
package net.thunderbird.core.preference.debugging
import net.thunderbird.core.preference.BuildConfig
actual val isDebug: Boolean = BuildConfig.DEBUG

View file

@ -0,0 +1,56 @@
package net.thunderbird.core.preference
import net.thunderbird.core.preference.debugging.DebuggingSettings
import net.thunderbird.core.preference.display.DisplaySettings
import net.thunderbird.core.preference.network.NetworkSettings
import net.thunderbird.core.preference.notification.NotificationPreference
import net.thunderbird.core.preference.privacy.PrivacySettings
/**
* Stores a snapshot of the app's general settings.
*
* When adding a setting here, make sure to also add it in these places:
* - [GeneralSettingsManager] (write function)
* - [GeneralSettingsDescriptions]
*/
// TODO: Move over settings from K9
data class GeneralSettings(
val network: NetworkSettings = NetworkSettings(),
val notification: NotificationPreference = NotificationPreference(),
val display: DisplaySettings = DisplaySettings(),
val privacy: PrivacySettings = PrivacySettings(),
val debugging: DebuggingSettings = DebuggingSettings(),
)
enum class BackgroundSync {
ALWAYS,
NEVER,
FOLLOW_SYSTEM_AUTO_SYNC,
}
enum class AppTheme {
LIGHT,
DARK,
FOLLOW_SYSTEM,
}
enum class SubTheme {
LIGHT,
DARK,
USE_GLOBAL,
}
enum class BackgroundOps {
ALWAYS,
NEVER,
WHEN_CHECKED_AUTO_SYNC,
}
/**
* Controls when to use the message list split view.
*/
enum class SplitViewMode {
ALWAYS,
NEVER,
WHEN_IN_LANDSCAPE,
}

View file

@ -0,0 +1,25 @@
package net.thunderbird.core.preference
import kotlinx.coroutines.flow.Flow
/**
* Retrieve and modify general settings.
*
*/
interface GeneralSettingsManager : PreferenceManager<GeneralSettings> {
@Deprecated(
message = "Use PreferenceManager<GeneralSettings>.getConfig() instead",
replaceWith = ReplaceWith(
expression = "getConfig()",
),
)
fun getSettings(): GeneralSettings
@Deprecated(
message = "Use PreferenceManager<GeneralSettings>.getConfigFlow() instead",
replaceWith = ReplaceWith(
expression = "getConfigFlow()",
),
)
fun getSettingsFlow(): Flow<GeneralSettings>
}

View file

@ -0,0 +1,22 @@
package net.thunderbird.core.preference
/**
* Broker to manage subscribers and notify them about changes in the preferences, when the
* [PreferenceChangePublisher] publishes a change.
*/
interface PreferenceChangeBroker {
/**
* Subscribe to preference changes.
*
* @param subscriber The subscriber to be notified about preference changes.
*/
fun subscribe(subscriber: PreferenceChangeSubscriber)
/**
* Unsubscribe from preference changes.
*
* @param subscriber The subscriber that no longer wants to be notified about preference changes.
*/
fun unsubscribe(subscriber: PreferenceChangeSubscriber)
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.core.preference
/**
* Publishes changes of preferences to all subscribers.
*/
interface PreferenceChangePublisher {
/**
* Publish a change in the preferences.
*/
fun publish()
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.core.preference
/**
* Subscribe to be notified about changes in the preferences.
*/
fun interface PreferenceChangeSubscriber {
/**
* Called when preferences change.
*/
fun receive()
}

View file

@ -0,0 +1,54 @@
package net.thunderbird.core.preference
import kotlinx.coroutines.flow.Flow
/**
* Interface for managing preferences of a specific type.
*
* This interface provides methods to save, retrieve, and observe changes to a configuration object.
*
* @param T The type of the configuration object being managed.
*/
interface PreferenceManager<T> {
/**
* Saves the given configuration.
*
* @param config The configuration of type [T] to be saved.
*/
fun save(config: T)
/**
* Retrieves a snapshot of the current configuration.
*
* **Note:** This function provides a one-time snapshot of the configuration.
* For observing configuration changes reactively, it is recommended to use [getConfigFlow] instead.
*
* @return The current configuration of type [T].
*/
fun getConfig(): T
/**
* Returns a [Flow] that emits the configuration whenever it changes.
*
* This allows observing changes to the configuration in a reactive way.
*
* @return A [Flow] of [T] representing the configuration.
*/
fun getConfigFlow(): Flow<T>
}
/**
* Updates the configuration by applying the given [updater] function to the current configuration.
*
* This function is an inline extension function for [PreferenceManager].
* It retrieves the current configuration, applies the [updater] function to it,
* and then saves the updated configuration.
*
* @param T The type of the configuration.
* @param updater A lambda function that takes the current configuration of type [T]
* and returns the updated configuration of type [T].
*/
inline fun <reified T> PreferenceManager<T>.update(updater: (T) -> T) {
val config = getConfig()
save(updater(config))
}

View file

@ -0,0 +1,3 @@
package net.thunderbird.core.preference.debugging
expect val isDebug: Boolean

View file

@ -0,0 +1,9 @@
package net.thunderbird.core.preference.debugging
const val DEBUGGING_SETTINGS_DEFAULT_IS_SYNC_LOGGING_ENABLED = false
val DEBUGGING_SETTINGS_DEFAULT_IS_DEBUGGING_LOGGING_ENABLED = isDebug
data class DebuggingSettings(
val isDebugLoggingEnabled: Boolean = DEBUGGING_SETTINGS_DEFAULT_IS_DEBUGGING_LOGGING_ENABLED,
val isSyncLoggingEnabled: Boolean = DEBUGGING_SETTINGS_DEFAULT_IS_SYNC_LOGGING_ENABLED,
)

View file

@ -0,0 +1,8 @@
package net.thunderbird.core.preference.debugging
import net.thunderbird.core.preference.PreferenceManager
const val KEY_ENABLE_DEBUG_LOGGING = "enableDebugLogging"
const val KEY_ENABLE_SYNC_DEBUG_LOGGING = "enableSyncDebugLogging"
interface DebuggingSettingsPreferenceManager : PreferenceManager<DebuggingSettings>

View file

@ -0,0 +1,32 @@
package net.thunderbird.core.preference.display
import net.thunderbird.core.preference.display.coreSettings.DisplayCoreSettings
import net.thunderbird.core.preference.display.inboxSettings.DisplayInboxSettings
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_ANIMATION = true
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CORRESPONDENT_NAMES = true
const val DISPLAY_SETTINGS_DEFAULT_SHOW_RECENT_CHANGES = true
const val DISPLAY_SETTINGS_DEFAULT_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG = true
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_NAME = false
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_PICTURE = true
const val DISPLAY_SETTINGS_DEFAULT_IS_CHANGE_CONTACT_NAME_COLOR = true
const val DISPLAY_SETTINGS_DEFAULT_IS_COLORIZE_MISSING_CONTACT_PICTURE = false
const val DISPLAY_SETTINGS_DEFAULT_IS_USE_BACKGROUND_AS_INDICATOR = false
const val DISPLAY_SETTINGS_DEFAULT_IS_USE_MESSAGE_VIEW_FIXED_WIDTH_FONT = false
const val DISPLAY_SETTINGS_DEFAULT_IS_AUTO_FIT_WIDTH = true
data class DisplaySettings(
val coreSettings: DisplayCoreSettings = DisplayCoreSettings(),
val inboxSettings: DisplayInboxSettings = DisplayInboxSettings(),
val isShowAnimations: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_ANIMATION,
val isShowCorrespondentNames: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CORRESPONDENT_NAMES,
val showRecentChanges: Boolean = DISPLAY_SETTINGS_DEFAULT_SHOW_RECENT_CHANGES,
val shouldShowSetupArchiveFolderDialog: Boolean = DISPLAY_SETTINGS_DEFAULT_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG,
val isShowContactName: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_NAME,
val isShowContactPicture: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_PICTURE,
val isChangeContactNameColor: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_CHANGE_CONTACT_NAME_COLOR,
val isColorizeMissingContactPictures: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_COLORIZE_MISSING_CONTACT_PICTURE,
val isUseBackgroundAsUnreadIndicator: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_USE_BACKGROUND_AS_INDICATOR,
val isUseMessageViewFixedWidthFont: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_USE_MESSAGE_VIEW_FIXED_WIDTH_FONT,
val isAutoFitWidth: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_AUTO_FIT_WIDTH,
)

View file

@ -0,0 +1,18 @@
package net.thunderbird.core.preference.display
import net.thunderbird.core.preference.PreferenceManager
const val KEY_SHOW_CONTACT_NAME = "showContactName"
const val KEY_SHOW_CORRESPONDENT_NAMES = "showCorrespondentNames"
const val KEY_SHOW_RECENT_CHANGES = "showRecentChanges"
const val KEY_THEME = "theme"
const val KEY_ANIMATION = "animations"
const val KEY_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG = "shouldShowSetupArchiveFolderDialog"
const val KEY_CHANGE_REGISTERED_NAME_COLOR = "changeRegisteredNameColor"
const val KEY_COLORIZE_MISSING_CONTACT_PICTURE = "colorizeMissingContactPictures"
const val KEY_USE_BACKGROUND_AS_UNREAD_INDICATOR = "isUseBackgroundAsUnreadIndicator"
const val KEY_MESSAGE_VIEW_FIXED_WIDTH_FONT = "messageViewFixedWidthFont"
const val KEY_AUTO_FIT_WIDTH = "autofitWidth"
const val KEY_SHOW_CONTACT_PICTURE = "showContactPicture"
interface DisplaySettingsPreferenceManager : PreferenceManager<DisplaySettings>

View file

@ -0,0 +1,20 @@
package net.thunderbird.core.preference.display.coreSettings
import net.thunderbird.core.preference.AppTheme
import net.thunderbird.core.preference.SplitViewMode
import net.thunderbird.core.preference.SubTheme
const val DISPLAY_SETTINGS_DEFAULT_APP_LANGUAGE = ""
const val DISPLAY_SETTINGS_DEFAULT_FIXED_MESSAGE_VIEW_THEME = true
val DISPLAY_SETTINGS_DEFAULT_APP_THEME = AppTheme.FOLLOW_SYSTEM
val DISPLAY_SETTINGS_DEFAULT_MESSAGE_COMPOSE_THEME = SubTheme.USE_GLOBAL
val DISPLAY_SETTINGS_DEFAULT_SPLIT_VIEW_MODE = SplitViewMode.NEVER
val DISPLAY_SETTINGS_DEFAULT_MESSAGE_VIEW_THEME = SubTheme.USE_GLOBAL
data class DisplayCoreSettings(
val fixedMessageViewTheme: Boolean = DISPLAY_SETTINGS_DEFAULT_FIXED_MESSAGE_VIEW_THEME,
val appTheme: AppTheme = DISPLAY_SETTINGS_DEFAULT_APP_THEME,
val messageViewTheme: SubTheme = DISPLAY_SETTINGS_DEFAULT_MESSAGE_VIEW_THEME,
val messageComposeTheme: SubTheme = DISPLAY_SETTINGS_DEFAULT_MESSAGE_COMPOSE_THEME,
val appLanguage: String = DISPLAY_SETTINGS_DEFAULT_APP_LANGUAGE,
val splitViewMode: SplitViewMode = DISPLAY_SETTINGS_DEFAULT_SPLIT_VIEW_MODE,
)

View file

@ -0,0 +1,11 @@
package net.thunderbird.core.preference.display.coreSettings
import net.thunderbird.core.preference.PreferenceManager
const val KEY_FIXED_MESSAGE_VIEW_THEME = "fixedMessageViewTheme"
const val KEY_MESSAGE_VIEW_THEME = "messageViewTheme"
const val KEY_MESSAGE_COMPOSE_THEME = "messageComposeTheme"
const val KEY_APP_LANGUAGE = "language"
const val KEY_SPLIT_VIEW_MODE = "splitViewMode"
interface DisplayCoreSettingsPreferenceManager : PreferenceManager<DisplayCoreSettings>

View file

@ -0,0 +1,17 @@
package net.thunderbird.core.preference.display.inboxSettings
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_UNIFIED_INBOX = false
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_STAR_COUNT = false
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_MESSAGE_LIST_STAR = true
const val DISPLAY_SETTINGS_DEFAULT_IS_MESSAGE_LIST_SENDER_ABOVE_SUBJECT = false
const val DISPLAY_SETTINGS_DEFAULT_IS_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST = true
const val DISPLAY_SETTINGS_DEFAULT_IS_THREAD_VIEW_ENABLED = true
data class DisplayInboxSettings(
val isShowUnifiedInbox: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_UNIFIED_INBOX,
val isShowStarredCount: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_STAR_COUNT,
val isShowMessageListStars: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_MESSAGE_LIST_STAR,
val isMessageListSenderAboveSubject: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_MESSAGE_LIST_SENDER_ABOVE_SUBJECT,
val isShowComposeButtonOnMessageList: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST,
val isThreadedViewEnabled: Boolean = DISPLAY_SETTINGS_DEFAULT_IS_THREAD_VIEW_ENABLED,
)

View file

@ -0,0 +1,12 @@
package net.thunderbird.core.preference.display.inboxSettings
import net.thunderbird.core.preference.PreferenceManager
const val KEY_MESSAGE_LIST_SENDER_ABOVE_SUBJECT = "messageListSenderAboveSubject"
const val KEY_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST = "showComposeButtonOnMessageList"
const val KEY_SHOW_MESSAGE_LIST_STARS = "messageListStars"
const val KEY_SHOW_STAR_COUNT = "showStarredCount"
const val KEY_SHOW_UNIFIED_INBOX = "showUnifiedInbox"
const val KEY_THREAD_VIEW_ENABLED = "isThreadedViewEnabled"
interface DisplayInboxSettingsPreferenceManager : PreferenceManager<DisplayInboxSettings>

View file

@ -0,0 +1,9 @@
package net.thunderbird.core.preference.network
import net.thunderbird.core.preference.BackgroundOps
val NETWORK_SETTINGS_DEFAULT_BACKGROUND_OPS = BackgroundOps.ALWAYS
data class NetworkSettings(
val backgroundOps: BackgroundOps = NETWORK_SETTINGS_DEFAULT_BACKGROUND_OPS,
)

View file

@ -0,0 +1,7 @@
package net.thunderbird.core.preference.network
import net.thunderbird.core.preference.PreferenceManager
const val KEY_BG_OPS = "backgroundOperations"
interface NetworkSettingsPreferenceManager : PreferenceManager<NetworkSettings>

View file

@ -0,0 +1,11 @@
package net.thunderbird.core.preference.notification
const val NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED = false
const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS = "21:00"
const val NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END = "7:00"
data class NotificationPreference(
val isQuietTimeEnabled: Boolean = NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED,
val quietTimeStarts: String = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS,
val quietTimeEnds: String = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END,
)

View file

@ -0,0 +1,9 @@
package net.thunderbird.core.preference.notification
import net.thunderbird.core.preference.PreferenceManager
const val KEY_QUIET_TIME_ENDS = "quietTimeEnds"
const val KEY_QUIET_TIME_STARTS = "quietTimeStarts"
const val KEY_QUIET_TIME_ENABLED = "quietTimeEnabled"
interface NotificationPreferenceManager : PreferenceManager<NotificationPreference>

View file

@ -0,0 +1,9 @@
package net.thunderbird.core.preference.privacy
const val PRIVACY_SETTINGS_DEFAULT_HIDE_TIME_ZONE = false
const val PRIVACY_SETTINGS_DEFAULT_HIDE_USER_AGENT = false
data class PrivacySettings(
val isHideTimeZone: Boolean = PRIVACY_SETTINGS_DEFAULT_HIDE_TIME_ZONE,
val isHideUserAgent: Boolean = PRIVACY_SETTINGS_DEFAULT_HIDE_USER_AGENT,
)

View file

@ -0,0 +1,8 @@
package net.thunderbird.core.preference.privacy
import net.thunderbird.core.preference.PreferenceManager
const val KEY_HIDE_TIME_ZONE = "hideTimeZone"
const val KEY_HIDE_USER_AGENT = "hideUserAgent"
interface PrivacySettingsPreferenceManager : PreferenceManager<PrivacySettings>

View file

@ -0,0 +1,108 @@
package net.thunderbird.core.preference.storage
interface Storage {
/**
* Checks if the storage is empty.
*
* @return true if the storage is empty, false otherwise.
*/
fun isEmpty(): Boolean
/**
* Checks if the storage contains a value for the given key.
*
* @param key The key to check.
* @return true if the storage contains a value for the given key, false otherwise.
*/
fun contains(key: String): Boolean
/**
* Returns a map of all key-value pairs in the storage.
*
* @return A map of all key-value pairs.
*/
fun getAll(): Map<String, String>
/**
* Returns the boolean value for the given key.
*
* @param key The key to look up.
* @param defValue The default value to return if the key is not found.
* @return The boolean value for the given key, or the default value if the key is not found.
*/
fun getBoolean(key: String, defValue: Boolean): Boolean
/**
* Returns the integer value for the given key.
*
* @param key The key to look up.
* @param defValue The default value to return if the key is not found.
* @return The integer value for the given key, or the default value if the key is not found.
*/
fun getInt(key: String, defValue: Int): Int
/**
* Returns the long value for the given key.
*
* @param key The key to look up.
* @param defValue The default value to return if the key is not found.
* @return The long value for the given key, or the default value if the key is not found.
*/
fun getLong(key: String, defValue: Long): Long
/**
* Returns the string value for the given key.
*
* @param key The key to look up.
* @return The string value for the given key.
* @throws NoSuchElementException if the key is not found.
*/
@Throws(NoSuchElementException::class)
fun getString(key: String): String
/**
* Returns the string value for the given key.
*
* @param key The key to look up.
* @param defValue The default value to return if the key is not found.
* @return The string value for the given key, or the default value if the key is not found.
*/
fun getStringOrDefault(key: String, defValue: String): String
/**
* Returns the string value for the given key, or null if the key is not found.
*
* @param key The key to look up.
* @return The string value for the given key, or null if the key is not found.
*/
fun getStringOrNull(key: String): String?
}
/**
* Returns the enum value for the given key, or the default value if the key is not found or the stored value
* is not a valid enum constant.
*
* @param T The enum type.
* @param key The key to look up.
* @param default The default enum value to return if the key is not found or the value is invalid.
* @return The enum value for the given key, or the default value.
* @throws IllegalArgumentException if the stored string value does not match any of the enum constants.
*/
@Throws(IllegalArgumentException::class)
inline fun <reified T : Enum<T>> Storage.getEnumOrDefault(key: String, default: T): T =
getStringOrNull(key)
?.let { value ->
try {
enumValueOf<T>(value)
} catch (e: IllegalArgumentException) {
throw IllegalArgumentException(
buildString {
append("Unable to convert stored key [$key] value [$value] ")
appendLine("to enum of type ${T::class.qualifiedName}.")
append("Valid values: ${enumValues<T>().joinToString()}")
},
e,
)
}
}
?: default

View file

@ -0,0 +1,61 @@
package net.thunderbird.core.preference.storage
/**
* Interface for editing the storage.
*
* This interface provides methods to put various types of values into the storage,
* remove values, and commit the changes.
*/
interface StorageEditor {
/**
* Puts a boolean value into the storage.
*
* @param key The key for the value.
* @param value The boolean value to put.
* @return The StorageEditor instance for chaining.
*/
fun putBoolean(key: String, value: Boolean): StorageEditor
/**
* Puts an integer value into the storage.
*
* @param key The key for the value.
* @param value The integer value to put.
* @return The StorageEditor instance for chaining.
*/
fun putInt(key: String, value: Int): StorageEditor
/**
* Puts a long value into the storage.
*
* @param key The key for the value.
* @param value The long value to put.
* @return The StorageEditor instance for chaining.
*/
fun putLong(key: String, value: Long): StorageEditor
/**
* Puts a string value into the storage.
*
* @param key The key for the value.
* @param value The string value to put. If null, the key will be removed.
* @return The StorageEditor instance for chaining.
*/
fun putString(key: String, value: String?): StorageEditor
/**
* Removes a value from the storage.
*
* @param key The key for the value to remove.
* @return The StorageEditor instance for chaining.
*/
fun remove(key: String): StorageEditor
/**
* Commits the changes made to the storage.
*
* @return true if the commit was successful, false otherwise.
*/
fun commit(): Boolean
}

View file

@ -0,0 +1,12 @@
package net.thunderbird.core.preference.storage
/**
* Extension functions for the [StorageEditor] interface to simplify putting enum values.
*
* @param T The type of the enum.
* @param key The key under which the enum value will be stored.
* @param value The enum value to be stored.
*/
inline fun <reified T : Enum<T>> StorageEditor.putEnum(key: String, value: T) {
putString(key, value.name)
}

View file

@ -0,0 +1,26 @@
package net.thunderbird.core.preference.storage
/**
* Represents a mechanism for persisting storage data.
*
* This interface provides methods to:
* - Load the current storage values.
* - Create a storage editor for applying updates to the storage.
*/
interface StoragePersister {
/**
* Loads the storage values.
*
* @return The loaded storage.
*/
fun loadValues(): Storage
/**
* Creates a storage editor for updating the storage.
*
* @param storageUpdater The updater to apply changes to the storage.
* @return A new instance of [StorageEditor].
*/
fun createStorageEditor(storageUpdater: StorageUpdater): StorageEditor
}

View file

@ -0,0 +1,14 @@
package net.thunderbird.core.preference.storage
/**
* Interface for updating the storage.
*/
fun interface StorageUpdater {
/**
* Updates the storage using the provided updater function.
*
* @param updater A function that takes the current storage and returns the updated storage.
*/
fun updateStorage(updater: (currentStorage: Storage) -> Storage)
}

View file

@ -0,0 +1,3 @@
package net.thunderbird.core.preference.debugging
actual val isDebug: Boolean = false

View file

@ -0,0 +1,17 @@
plugins {
id(ThunderbirdPlugins.Library.kmp)
}
android {
namespace = "net.thunderbird.core.preference.impl"
}
kotlin {
sourceSets {
commonMain.dependencies {
api(projects.core.preference.api)
implementation(projects.core.logging.api)
}
}
}

View file

@ -0,0 +1,28 @@
package net.thunderbird.core.preference
class DefaultPreferenceChangeBroker(
private val subscribers: MutableSet<PreferenceChangeSubscriber> = mutableSetOf(),
) : PreferenceChangeBroker, PreferenceChangePublisher {
private val lock = Any()
override fun subscribe(subscriber: PreferenceChangeSubscriber) {
synchronized(lock) {
subscribers.add(subscriber)
}
}
override fun unsubscribe(subscriber: PreferenceChangeSubscriber) {
synchronized(lock) {
subscribers.remove(subscriber)
}
}
override fun publish() {
val currentSubscribers = synchronized(lock) { HashSet(subscribers) }
for (subscriber in currentSubscribers) {
subscriber.receive()
}
}
}

View file

@ -0,0 +1,72 @@
package net.thunderbird.core.preference.debugging
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.LogLevel
import net.thunderbird.core.logging.LogLevelManager
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
private const val TAG = "DefaultDebuggingSettingsPreferenceManager"
class DefaultDebuggingSettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val logLevelManager: LogLevelManager,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : DebuggingSettingsPreferenceManager {
private val configState: MutableStateFlow<DebuggingSettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): DebuggingSettings = configState.value
override fun getConfigFlow(): Flow<DebuggingSettings> = configState
override fun save(config: DebuggingSettings) {
logger.debug(TAG) { "save() called with: config = $config" }
writeConfig(config)
configState.update { config.also(::updateDebugLogLevel) }
}
private fun loadConfig(): DebuggingSettings = DebuggingSettings(
isDebugLoggingEnabled = storage.getBoolean(
KEY_ENABLE_DEBUG_LOGGING,
DEBUGGING_SETTINGS_DEFAULT_IS_DEBUGGING_LOGGING_ENABLED,
),
isSyncLoggingEnabled = storage.getBoolean(
KEY_ENABLE_SYNC_DEBUG_LOGGING,
DEBUGGING_SETTINGS_DEFAULT_IS_SYNC_LOGGING_ENABLED,
),
).also(::updateDebugLogLevel)
private fun writeConfig(config: DebuggingSettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putBoolean(KEY_ENABLE_DEBUG_LOGGING, config.isDebugLoggingEnabled)
storageEditor.putBoolean(KEY_ENABLE_SYNC_DEBUG_LOGGING, config.isSyncLoggingEnabled)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
private fun updateDebugLogLevel(config: DebuggingSettings) {
if (config.isDebugLoggingEnabled) {
logLevelManager.override(LogLevel.DEBUG)
} else {
logLevelManager.restoreDefault()
}
}
}

View file

@ -0,0 +1,131 @@
package net.thunderbird.core.preference.display
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.display.coreSettings.DisplayCoreSettingsPreferenceManager
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
private const val TAG = "DefaultDisplaySettingsPreferenceManager"
class DefaultDisplaySettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
private val coreSettingsPreferenceManager: DisplayCoreSettingsPreferenceManager,
) : DisplaySettingsPreferenceManager {
private val configState: MutableStateFlow<DisplaySettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): DisplaySettings = configState.value
override fun getConfigFlow(): Flow<DisplaySettings> = configState
override fun save(config: DisplaySettings) {
logger.debug(TAG) { "save() called with: config = $config" }
coreSettingsPreferenceManager.save(config.coreSettings)
writeConfig(config)
configState.update { config }
}
private fun loadConfig(): DisplaySettings = DisplaySettings(
coreSettings = coreSettingsPreferenceManager.getConfig(),
showRecentChanges = storage.getBoolean(
KEY_SHOW_RECENT_CHANGES,
DISPLAY_SETTINGS_DEFAULT_SHOW_RECENT_CHANGES,
),
shouldShowSetupArchiveFolderDialog = storage.getBoolean(
KEY_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG,
DISPLAY_SETTINGS_DEFAULT_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG,
),
isColorizeMissingContactPictures = storage.getBoolean(
KEY_COLORIZE_MISSING_CONTACT_PICTURE,
DISPLAY_SETTINGS_DEFAULT_IS_COLORIZE_MISSING_CONTACT_PICTURE,
),
isChangeContactNameColor = storage.getBoolean(
KEY_CHANGE_REGISTERED_NAME_COLOR,
DISPLAY_SETTINGS_DEFAULT_IS_CHANGE_CONTACT_NAME_COLOR,
),
isUseBackgroundAsUnreadIndicator = storage.getBoolean(
KEY_USE_BACKGROUND_AS_UNREAD_INDICATOR,
DISPLAY_SETTINGS_DEFAULT_IS_USE_BACKGROUND_AS_INDICATOR,
),
isUseMessageViewFixedWidthFont = storage.getBoolean(
KEY_MESSAGE_VIEW_FIXED_WIDTH_FONT,
DISPLAY_SETTINGS_DEFAULT_IS_USE_MESSAGE_VIEW_FIXED_WIDTH_FONT,
),
isAutoFitWidth = storage.getBoolean(
KEY_AUTO_FIT_WIDTH,
DISPLAY_SETTINGS_DEFAULT_IS_AUTO_FIT_WIDTH,
),
isShowAnimations = storage.getBoolean(
KEY_ANIMATION,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_ANIMATION,
),
isShowCorrespondentNames = storage.getBoolean(
KEY_SHOW_CORRESPONDENT_NAMES,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CORRESPONDENT_NAMES,
),
isShowContactName = storage.getBoolean(
KEY_SHOW_CONTACT_NAME,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_NAME,
),
isShowContactPicture = storage.getBoolean(
KEY_SHOW_CONTACT_PICTURE,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_CONTACT_PICTURE,
),
)
private fun writeConfig(config: DisplaySettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putBoolean(
KEY_CHANGE_REGISTERED_NAME_COLOR,
config.isChangeContactNameColor,
)
storageEditor.putBoolean(
KEY_COLORIZE_MISSING_CONTACT_PICTURE,
config.isColorizeMissingContactPictures,
)
storageEditor.putBoolean(
KEY_SHOULD_SHOW_SETUP_ARCHIVE_FOLDER_DIALOG,
config.shouldShowSetupArchiveFolderDialog,
)
storageEditor.putBoolean(KEY_SHOW_CONTACT_NAME, config.isShowContactName)
storageEditor.putBoolean(
KEY_SHOW_CORRESPONDENT_NAMES,
config.isShowCorrespondentNames,
)
storageEditor.putBoolean(KEY_SHOW_RECENT_CHANGES, config.showRecentChanges)
storageEditor.putBoolean(KEY_ANIMATION, config.isShowAnimations)
storageEditor.putBoolean(
KEY_SHOW_CONTACT_PICTURE,
config.isShowContactPicture,
)
storageEditor.putBoolean(
KEY_USE_BACKGROUND_AS_UNREAD_INDICATOR,
config.isUseBackgroundAsUnreadIndicator,
)
storageEditor.putBoolean(
KEY_MESSAGE_VIEW_FIXED_WIDTH_FONT,
config.isUseMessageViewFixedWidthFont,
)
storageEditor.putBoolean(KEY_AUTO_FIT_WIDTH, config.isAutoFitWidth)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
}

View file

@ -0,0 +1,89 @@
package net.thunderbird.core.preference.display.coreSettings
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.display.KEY_THEME
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
import net.thunderbird.core.preference.storage.getEnumOrDefault
import net.thunderbird.core.preference.storage.putEnum
private const val TAG = "DefaultDisplayCoreSettingsPreferenceManager"
class DefaultDisplayCoreSettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : DisplayCoreSettingsPreferenceManager {
private val configState: MutableStateFlow<DisplayCoreSettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): DisplayCoreSettings = configState.value
override fun getConfigFlow(): Flow<DisplayCoreSettings> = configState
override fun save(config: DisplayCoreSettings) {
logger.debug(TAG) { "save() called with: config = $config" }
writeConfig(config)
configState.update { config }
}
private fun loadConfig(): DisplayCoreSettings = DisplayCoreSettings(
fixedMessageViewTheme = storage.getBoolean(
KEY_FIXED_MESSAGE_VIEW_THEME,
DISPLAY_SETTINGS_DEFAULT_FIXED_MESSAGE_VIEW_THEME,
),
appTheme = storage.getEnumOrDefault(KEY_THEME, DISPLAY_SETTINGS_DEFAULT_APP_THEME),
messageViewTheme = storage.getEnumOrDefault(
KEY_MESSAGE_VIEW_THEME,
DISPLAY_SETTINGS_DEFAULT_MESSAGE_VIEW_THEME,
),
messageComposeTheme = storage.getEnumOrDefault(
KEY_MESSAGE_COMPOSE_THEME,
DISPLAY_SETTINGS_DEFAULT_MESSAGE_COMPOSE_THEME,
),
appLanguage = storage.getStringOrDefault(
KEY_APP_LANGUAGE,
DISPLAY_SETTINGS_DEFAULT_APP_LANGUAGE,
),
splitViewMode = storage.getEnumOrDefault(
KEY_SPLIT_VIEW_MODE,
DISPLAY_SETTINGS_DEFAULT_SPLIT_VIEW_MODE,
),
)
private fun writeConfig(config: DisplayCoreSettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putEnum(KEY_THEME, config.appTheme)
storageEditor.putEnum(KEY_MESSAGE_VIEW_THEME, config.messageViewTheme)
storageEditor.putEnum(
KEY_MESSAGE_COMPOSE_THEME,
config.messageComposeTheme,
)
storageEditor.putBoolean(
KEY_FIXED_MESSAGE_VIEW_THEME,
config.fixedMessageViewTheme,
)
storageEditor.putString(KEY_APP_LANGUAGE, config.appLanguage)
storageEditor.putEnum(KEY_SPLIT_VIEW_MODE, config.splitViewMode)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
}

View file

@ -0,0 +1,95 @@
package net.thunderbird.core.preference.display.inboxSettings
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
private const val TAG = "DefaultDisplayInboxSettingsPreferenceManager"
class DefaultDisplayInboxSettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : DisplayInboxSettingsPreferenceManager {
private val configState: MutableStateFlow<DisplayInboxSettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): DisplayInboxSettings = configState.value
override fun getConfigFlow(): Flow<DisplayInboxSettings> = configState
override fun save(config: DisplayInboxSettings) {
logger.debug(TAG) { "save() called with: config = $config" }
writeConfig(config)
configState.update { config }
}
private fun loadConfig(): DisplayInboxSettings = DisplayInboxSettings(
isShowUnifiedInbox = storage.getBoolean(
KEY_SHOW_UNIFIED_INBOX,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_UNIFIED_INBOX,
),
isShowComposeButtonOnMessageList = storage.getBoolean(
KEY_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST,
),
isThreadedViewEnabled = storage.getBoolean(
KEY_THREAD_VIEW_ENABLED,
DISPLAY_SETTINGS_DEFAULT_IS_THREAD_VIEW_ENABLED,
),
isShowStarredCount = storage.getBoolean(
KEY_SHOW_STAR_COUNT,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_STAR_COUNT,
),
isShowMessageListStars = storage.getBoolean(
KEY_SHOW_MESSAGE_LIST_STARS,
DISPLAY_SETTINGS_DEFAULT_IS_SHOW_MESSAGE_LIST_STAR,
),
isMessageListSenderAboveSubject = storage.getBoolean(
KEY_MESSAGE_LIST_SENDER_ABOVE_SUBJECT,
DISPLAY_SETTINGS_DEFAULT_IS_MESSAGE_LIST_SENDER_ABOVE_SUBJECT,
),
)
private fun writeConfig(config: DisplayInboxSettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putBoolean(
KEY_MESSAGE_LIST_SENDER_ABOVE_SUBJECT,
config.isMessageListSenderAboveSubject,
)
storageEditor.putBoolean(
KEY_SHOW_MESSAGE_LIST_STARS,
config.isShowMessageListStars,
)
storageEditor.putBoolean(
KEY_SHOW_COMPOSE_BUTTON_ON_MESSAGE_LIST,
config.isShowComposeButtonOnMessageList,
)
storageEditor.putBoolean(
KEY_THREAD_VIEW_ENABLED,
config.isThreadedViewEnabled,
)
storageEditor.putBoolean(KEY_SHOW_UNIFIED_INBOX, config.isShowUnifiedInbox)
storageEditor.putBoolean(KEY_SHOW_STAR_COUNT, config.isShowStarredCount)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
}

View file

@ -0,0 +1,55 @@
package net.thunderbird.core.preference.network
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
import net.thunderbird.core.preference.storage.getEnumOrDefault
import net.thunderbird.core.preference.storage.putEnum
private const val TAG = "DefaultNetworkSettingsPreferenceManager"
class DefaultNetworkSettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : NetworkSettingsPreferenceManager {
private val configState: MutableStateFlow<NetworkSettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): NetworkSettings = configState.value
override fun getConfigFlow(): Flow<NetworkSettings> = configState
override fun save(config: NetworkSettings) {
logger.debug(TAG) { "save() called with: config = $config" }
writeConfig(config)
configState.update { config }
}
private fun loadConfig(): NetworkSettings = NetworkSettings(
backgroundOps = storage.getEnumOrDefault(KEY_BG_OPS, NETWORK_SETTINGS_DEFAULT_BACKGROUND_OPS),
)
private fun writeConfig(config: NetworkSettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putEnum(KEY_BG_OPS, config.backgroundOps)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
}

View file

@ -0,0 +1,64 @@
package net.thunderbird.core.preference.notification
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
private const val TAG = "DefaultNotificationPreferenceManager"
class DefaultNotificationPreferenceManager(
private val logger: Logger,
storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : NotificationPreferenceManager {
private val mutex = Mutex()
private val configState = MutableStateFlow(
value = NotificationPreference(
isQuietTimeEnabled = storage.getBoolean(
key = KEY_QUIET_TIME_ENABLED,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_IS_QUIET_TIME_ENABLED,
),
quietTimeStarts = storage.getStringOrDefault(
key = KEY_QUIET_TIME_STARTS,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_STARTS,
),
quietTimeEnds = storage.getStringOrDefault(
key = KEY_QUIET_TIME_ENDS,
defValue = NOTIFICATION_PREFERENCE_DEFAULT_QUIET_TIME_END,
),
),
)
override fun getConfig(): NotificationPreference = configState.value
override fun getConfigFlow(): Flow<NotificationPreference> = configState
override fun save(config: NotificationPreference) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putString(KEY_QUIET_TIME_ENDS, config.quietTimeEnds)
storageEditor.putString(KEY_QUIET_TIME_STARTS, config.quietTimeStarts)
storageEditor.putBoolean(
KEY_QUIET_TIME_ENABLED,
config.isQuietTimeEnabled,
)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
configState.update { config }
}
}
}

View file

@ -0,0 +1,61 @@
package net.thunderbird.core.preference.privacy
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import net.thunderbird.core.logging.Logger
import net.thunderbird.core.preference.storage.Storage
import net.thunderbird.core.preference.storage.StorageEditor
private const val TAG = "DefaultPrivacySettingsPreferenceManager"
class DefaultPrivacySettingsPreferenceManager(
private val logger: Logger,
private val storage: Storage,
private val storageEditor: StorageEditor,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
private var scope: CoroutineScope = CoroutineScope(SupervisorJob()),
) : PrivacySettingsPreferenceManager {
private val configState: MutableStateFlow<PrivacySettings> = MutableStateFlow(value = loadConfig())
private val mutex = Mutex()
override fun getConfig(): PrivacySettings = configState.value
override fun getConfigFlow(): Flow<PrivacySettings> = configState
override fun save(config: PrivacySettings) {
logger.debug(TAG) { "save() called with: config = $config" }
writeConfig(config)
configState.update { config }
}
private fun loadConfig(): PrivacySettings = PrivacySettings(
isHideTimeZone = storage.getBoolean(
key = KEY_HIDE_TIME_ZONE,
defValue = PRIVACY_SETTINGS_DEFAULT_HIDE_TIME_ZONE,
),
isHideUserAgent = storage.getBoolean(
key = KEY_HIDE_USER_AGENT,
defValue = PRIVACY_SETTINGS_DEFAULT_HIDE_USER_AGENT,
),
)
private fun writeConfig(config: PrivacySettings) {
logger.debug(TAG) { "writeConfig() called with: config = $config" }
scope.launch(ioDispatcher) {
mutex.withLock {
storageEditor.putBoolean(KEY_HIDE_TIME_ZONE, config.isHideTimeZone)
storageEditor.putBoolean(KEY_HIDE_USER_AGENT, config.isHideUserAgent)
storageEditor.commit().also { commited ->
logger.verbose(TAG) { "writeConfig: storageEditor.commit() resulted in: $commited" }
}
}
}
}
}

View file

@ -0,0 +1,56 @@
package net.thunderbird.core.preference.storage
import net.thunderbird.core.logging.Logger
class InMemoryStorage(
private val values: Map<String, String>,
private val logger: Logger,
) : Storage {
override fun isEmpty(): Boolean = values.isEmpty()
override fun contains(key: String): Boolean = values.contains(key)
override fun getAll(): Map<String, String> = values
override fun getBoolean(key: String, defValue: Boolean): Boolean =
values[key]
?.toBoolean()
?: defValue
override fun getInt(key: String, defValue: Int): Int {
val value = values[key] ?: return defValue
return try {
value.toInt()
} catch (e: NumberFormatException) {
logger.error(
message = { "Could not parse int" },
throwable = e,
)
defValue
}
}
override fun getLong(key: String, defValue: Long): Long {
val value = values[key] ?: return defValue
return try {
value.toLong()
} catch (e: NumberFormatException) {
logger.error(
message = { "Could not parse long" },
throwable = e,
)
defValue
}
}
@Throws(NoSuchElementException::class)
override fun getString(key: String): String =
values.getValue(key)
override fun getStringOrDefault(key: String, defValue: String): String =
getStringOrNull(key) ?: defValue
override fun getStringOrNull(key: String): String? =
values[key]
}

View file

@ -0,0 +1,49 @@
package net.thunderbird.core.preference
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.doesNotContain
import assertk.assertions.isEqualTo
import kotlin.test.Test
class DefaultPreferenceChangeBrokerTest {
@Test
fun `subscribe should add subscriber`() {
val subscriber = PreferenceChangeSubscriber { }
val subscribers = mutableSetOf<PreferenceChangeSubscriber>()
val broker = DefaultPreferenceChangeBroker(subscribers)
broker.subscribe(subscriber)
assertThat(subscribers.size).isEqualTo(1)
assertThat(subscribers).contains(subscriber)
}
@Test
fun `unsubscribe should remove subscriber`() {
val subscriber = PreferenceChangeSubscriber { }
val subscribers = mutableSetOf<PreferenceChangeSubscriber>(subscriber)
val broker = DefaultPreferenceChangeBroker(subscribers)
broker.unsubscribe(subscriber)
assertThat(subscribers.size).isEqualTo(0)
assertThat(subscribers).doesNotContain(subscriber)
}
@Test
fun `publish should notify subscribers`() {
var received = false
val subscriber = PreferenceChangeSubscriber { received = true }
var receivedOther = false
val otherSubscriber = PreferenceChangeSubscriber { receivedOther = true }
val subscribers = mutableSetOf<PreferenceChangeSubscriber>(subscriber, otherSubscriber)
val broker = DefaultPreferenceChangeBroker(subscribers)
broker.publish()
assertThat(received).isEqualTo(true)
assertThat(receivedOther).isEqualTo(true)
}
}