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