package com.fsck.k9.preferences;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import android.util.Log;
import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
/*
* TODO:
* - use the default values defined in GlobalSettings and AccountSettings when creating new
* accounts
* - think of a better way to validate enums than to use the resource arrays (i.e. get rid of
* ResourceArrayValidator); maybe even use the settings description for the settings UI
* - add unit test that validates the default values are actually valid according to the validator
*/
public class Settings {
/**
* Version number of global and account settings.
*
*
* This value is used as "version" attribute in the export file. It needs to be incremented
* when a global or account setting is added or removed, or when the format of a setting
* is changed (e.g. add a value to an enum).
*
*
* @see SettingsExporter
*/
public static final int VERSION = 4;
public static Map validate(int version, Map> settings,
Map importedSettings, boolean useDefaultValues) {
Map validatedSettings = new HashMap();
for (Map.Entry> versionedSetting :
settings.entrySet()) {
Entry setting =
versionedSetting.getValue().floorEntry(version);
if (setting == null) {
continue;
}
String key = versionedSetting.getKey();
SettingsDescription desc = setting.getValue();
boolean useDefaultValue;
if (!importedSettings.containsKey(key)) {
Log.v(K9.LOG_TAG, "Key \"" + key + "\" wasn't found in the imported file." +
((useDefaultValues) ? " Using default value." : ""));
useDefaultValue = useDefaultValues;
} else {
String prettyValue = importedSettings.get(key);
try {
Object internalValue = desc.fromPrettyString(prettyValue);
validatedSettings.put(key, internalValue);
useDefaultValue = false;
} catch (InvalidSettingValueException e) {
Log.v(K9.LOG_TAG, "Key \"" + key + "\" has invalid value \"" + prettyValue +
"\" in imported file. " +
((useDefaultValues) ? "Using default value." : "Skipping."));
useDefaultValue = useDefaultValues;
}
}
if (useDefaultValue) {
Object defaultValue = desc.getDefaultValue();
validatedSettings.put(key, defaultValue);
}
}
return validatedSettings;
}
/**
* Upgrade settings using the settings structure and/or special upgrade code.
*
* @param version
* The content version of the settings in {@code validatedSettings}.
* @param upgraders
* A map of {@link SettingsUpgrader}s for nontrivial settings upgrades.
* @param settings
* The structure describing the different settings, possibly containing multiple
* versions.
* @param validatedSettings
* The settings as returned by {@link Settings#validate(int, Map, Map, boolean)}.
* This map is modified and contains the upgraded settings when this method returns.
*
* @return A set of setting names that were removed during the upgrade process or {@code null}
* if none were removed.
*/
public static Set upgrade(int version, Map upgraders,
Map> settings,
Map validatedSettings) {
Map upgradedSettings = validatedSettings;
Set deletedSettings = null;
for (int toVersion = version + 1; toVersion <= VERSION; toVersion++) {
// Check if there's an SettingsUpgrader for that version
SettingsUpgrader upgrader = upgraders.get(toVersion);
if (upgrader != null) {
deletedSettings = upgrader.upgrade(upgradedSettings);
}
// Deal with settings that don't need special upgrade code
for (Entry> versions :
settings.entrySet()) {
String settingName = versions.getKey();
TreeMap versionedSettings = versions.getValue();
// Handle newly added settings
if (versionedSettings.firstKey().intValue() == toVersion) {
// Check if it was already added to upgradedSettings by the SettingsUpgrader
if (!upgradedSettings.containsKey(settingName)) {
// Insert default value to upgradedSettings
SettingsDescription setting = versionedSettings.firstEntry().getValue();
Object defaultValue = setting.getDefaultValue();
upgradedSettings.put(settingName, defaultValue);
if (K9.DEBUG) {
String prettyValue = setting.toPrettyString(defaultValue);
Log.v(K9.LOG_TAG, "Added new setting \"" + settingName +
"\" with default value \"" + prettyValue + "\"");
}
}
}
// Handle removed settings
Entry lastEntry = versionedSettings.lastEntry();
if (lastEntry.getKey().intValue() == toVersion && lastEntry.getValue() == null) {
upgradedSettings.remove(settingName);
if (deletedSettings == null) {
deletedSettings = new HashSet();
}
deletedSettings.add(settingName);
if (K9.DEBUG) {
Log.v(K9.LOG_TAG, "Removed setting \"" + settingName + "\"");
}
}
}
}
return deletedSettings;
}
/**
* Convert settings from the internal representation to the string representation used in the
* preference storage.
*
* @param settings
* The map of settings to convert.
* @param settingDescriptions
* The structure containing the {@link SettingsDescription} objects that will be used
* to convert the setting values.
*
* @return The settings converted to the string representation used in the preference storage.
*/
public static Map convert(Map settings,
Map> settingDescriptions) {
Map serializedSettings = new HashMap();
for (Entry setting : settings.entrySet()) {
String settingName = setting.getKey();
Object internalValue = setting.getValue();
SettingsDescription settingDesc =
settingDescriptions.get(settingName).lastEntry().getValue();
if (settingDesc != null) {
String stringValue = settingDesc.toString(internalValue);
serializedSettings.put(settingName, stringValue);
} else {
if (K9.DEBUG) {
Log.w(K9.LOG_TAG, "Settings.serialize() called with a setting that should " +
"have been removed: " + settingName);
}
}
}
return serializedSettings;
}
/**
* Creates a {@link TreeMap} linking version numbers to {@link SettingsDescription} instances.
*
*
* This {@code TreeMap} is used to quickly find the {@code SettingsDescription} belonging to a
* content version as read by {@link SettingsImporter}. See e.g.
* {@link Settings#validate(int, Map, Map, boolean)}.
*
*
* @param versionDescriptions
* A list of descriptions for a specific setting mapped to version numbers. Never
* {@code null}.
*
* @return A {@code TreeMap} using the version number as key, the {@code SettingsDescription}
* as value.
*/
public static TreeMap versions(
V... versionDescriptions) {
TreeMap map = new TreeMap();
for (V v : versionDescriptions) {
map.put(v.version, v.description);
}
return map;
}
/**
* Indicates an invalid setting value.
*
* @see SettingsDescription#fromString(String)
* @see SettingsDescription#fromPrettyString(String)
*/
public static class InvalidSettingValueException extends Exception {
private static final long serialVersionUID = 1L;
}
/**
* Describes a setting.
*
*
* Instances of this class are used to convert the string representations of setting values to
* an internal representation (e.g. an integer) and back.
*
* Currently we use two different string representations:
*
*
* -
* The one that is used by the internal preference {@link Storage}. It is usually obtained by
* calling {@code toString()} on the internal representation of the setting value (see e.g.
* {@link K9#save(android.content.SharedPreferences.Editor)}).
*
* -
* The "pretty" version that is used by the import/export settings file (e.g. colors are
* exported in #rrggbb format instead of a integer string like "-8734021").
*
*
*
* Note:
* For the future we should aim to get rid of the "internal" string representation. The
* "pretty" version makes reading a database dump easier and the performance impact should be
* negligible.
*
*/
public static abstract class SettingsDescription {
/**
* The setting's default value (internal representation).
*/
protected Object mDefaultValue;
public SettingsDescription(Object defaultValue) {
mDefaultValue = defaultValue;
}
/**
* Get the default value.
*
* @return The internal representation of the default value.
*/
public Object getDefaultValue() {
return mDefaultValue;
}
/**
* Convert a setting's value to the string representation.
*
* @param value
* The internal representation of a setting's value.
*
* @return The string representation of {@code value}.
*/
public String toString(Object value) {
return value.toString();
}
/**
* Parse the string representation of a setting's value .
*
* @param value
* The string representation of a setting's value.
*
* @return The internal representation of the setting's value.
*
* @throws InvalidSettingValueException
* If {@code value} contains an invalid value.
*/
public abstract Object fromString(String value) throws InvalidSettingValueException;
/**
* Convert a setting value to the "pretty" string representation.
*
* @param value
* The setting's value.
*
* @return A pretty-printed version of the setting's value.
*/
public String toPrettyString(Object value) {
return toString(value);
}
/**
* Convert the pretty-printed version of a setting's value to the internal representation.
*
* @param value
* The pretty-printed version of the setting's value. See
* {@link #toPrettyString(Object)}.
*
* @return The internal representation of the setting's value.
*
* @throws InvalidSettingValueException
* If {@code value} contains an invalid value.
*/
public Object fromPrettyString(String value) throws InvalidSettingValueException {
return fromString(value);
}
}
/**
* Container to hold a {@link SettingsDescription} instance and a version number.
*
* @see Settings#versions(V...)
*/
public static class V {
public final Integer version;
public final SettingsDescription description;
public V(Integer version, SettingsDescription description) {
this.version = version;
this.description = description;
}
}
/**
* Used for a nontrivial settings upgrade.
*
* @see Settings#upgrade(int, Map, Map, Map)
*/
public interface SettingsUpgrader {
/**
* Upgrade the provided settings.
*
* @param settings
* The settings to upgrade. This map is modified and contains the upgraded
* settings when this method returns.
*
* @return A set of setting names that were removed during the upgrade process or
* {@code null} if none were removed.
*/
public Set upgrade(Map settings);
}
/**
* A string setting.
*/
public static class StringSetting extends SettingsDescription {
public StringSetting(String defaultValue) {
super(defaultValue);
}
@Override
public Object fromString(String value) {
return value;
}
}
/**
* A boolean setting.
*/
public static class BooleanSetting extends SettingsDescription {
public BooleanSetting(boolean defaultValue) {
super(defaultValue);
}
@Override
public Object fromString(String value) throws InvalidSettingValueException {
if (Boolean.TRUE.toString().equals(value)) {
return true;
} else if (Boolean.FALSE.toString().equals(value)) {
return false;
}
throw new InvalidSettingValueException();
}
}
/**
* A color setting.
*/
public static class ColorSetting extends SettingsDescription {
public ColorSetting(int defaultValue) {
super(defaultValue);
}
@Override
public Object fromString(String value) throws InvalidSettingValueException {
try {
return Integer.parseInt(value);
} catch (NumberFormatException e) {
throw new InvalidSettingValueException();
}
}
@Override
public String toPrettyString(Object value) {
int color = ((Integer) value) & 0x00FFFFFF;
return String.format("#%06x", color);
}
@Override
public Object fromPrettyString(String value) throws InvalidSettingValueException {
try {
if (value.length() == 7) {
return Integer.parseInt(value.substring(1), 16) | 0xFF000000;
}
} catch (NumberFormatException e) { /* do nothing */ }
throw new InvalidSettingValueException();
}
}
/**
* An {@code Enum} setting.
*
*
* {@link Enum#toString()} is used to obtain the "pretty" string representation.
*
*/
public static class EnumSetting extends SettingsDescription {
private Class extends Enum>> mEnumClass;
public EnumSetting(Class extends Enum>> enumClass, Object defaultValue) {
super(defaultValue);
mEnumClass = enumClass;
}
@SuppressWarnings("unchecked")
@Override
public Object fromString(String value) throws InvalidSettingValueException {
try {
return Enum.valueOf((Class extends Enum>)mEnumClass, value);
} catch (Exception e) {
throw new InvalidSettingValueException();
}
}
}
/**
* A setting that has multiple valid values but doesn't use an {@link Enum} internally.
*
* @param
* The type of the internal representation (e.g. {@code Integer}).
*/
public abstract static class PseudoEnumSetting extends SettingsDescription {
public PseudoEnumSetting(Object defaultValue) {
super(defaultValue);
}
protected abstract Map getMapping();
@Override
public String toPrettyString(Object value) {
return getMapping().get(value);
}
@Override
public Object fromPrettyString(String value) throws InvalidSettingValueException {
for (Entry entry : getMapping().entrySet()) {
if (entry.getValue().equals(value)) {
return entry.getKey();
}
}
throw new InvalidSettingValueException();
}
}
/**
* A font size setting.
*/
public static class FontSizeSetting extends PseudoEnumSetting {
private final Map mMapping;
public FontSizeSetting(int defaultValue) {
super(defaultValue);
Map mapping = new HashMap();
mapping.put(FontSizes.FONT_10DIP, "tiniest");
mapping.put(FontSizes.FONT_12DIP, "tiny");
mapping.put(FontSizes.SMALL, "smaller");
mapping.put(FontSizes.FONT_16DIP, "small");
mapping.put(FontSizes.MEDIUM, "medium");
mapping.put(FontSizes.FONT_20DIP, "large");
mapping.put(FontSizes.LARGE, "larger");
mMapping = Collections.unmodifiableMap(mapping);
}
@Override
protected Map getMapping() {
return mMapping;
}
@Override
public Object fromString(String value) throws InvalidSettingValueException {
try {
Integer fontSize = Integer.parseInt(value);
if (mMapping.containsKey(fontSize)) {
return fontSize;
}
} catch (NumberFormatException e) { /* do nothing */ }
throw new InvalidSettingValueException();
}
}
/**
* A {@link android.webkit.WebView} font size setting.
*/
public static class WebFontSizeSetting extends PseudoEnumSetting {
private final Map mMapping;
public WebFontSizeSetting(int defaultValue) {
super(defaultValue);
Map mapping = new HashMap();
mapping.put(1, "smallest");
mapping.put(2, "smaller");
mapping.put(3, "normal");
mapping.put(4, "larger");
mapping.put(5, "largest");
mMapping = Collections.unmodifiableMap(mapping);
}
@Override
protected Map getMapping() {
return mMapping;
}
@Override
public Object fromString(String value) throws InvalidSettingValueException {
try {
Integer fontSize = Integer.parseInt(value);
if (mMapping.containsKey(fontSize)) {
return fontSize;
}
} catch (NumberFormatException e) { /* do nothing */ }
throw new InvalidSettingValueException();
}
}
/**
* An integer settings whose values a limited to a certain range.
*/
public static class IntegerRangeSetting extends SettingsDescription {
private int mStart;
private int mEnd;
public IntegerRangeSetting(int start, int end, int defaultValue) {
super(defaultValue);
mStart = start;
mEnd = end;
}
@Override
public Object fromString(String value) throws InvalidSettingValueException {
try {
int intValue = Integer.parseInt(value);
if (mStart <= intValue && intValue <= mEnd) {
return intValue;
}
} catch (NumberFormatException e) { /* do nothing */ }
throw new InvalidSettingValueException();
}
}
}