package com.fsck.k9.preferences;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;
import org.xmlpull.v1.XmlSerializer;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.util.Log;
import android.util.Xml;
import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
import com.fsck.k9.preferences.Settings.SettingsDescription;
public class SettingsExporter {
private static final String EXPORT_FILENAME = "settings.k9s";
/**
* File format version number.
*
*
* Increment this if you need to change the structure of the settings file. When you do this
* remember that we also have to be able to handle old file formats. So have fun adding support
* for that to {@link SettingsImporter} :)
*
*/
public static final int FILE_FORMAT_VERSION = 1;
public static final String ROOT_ELEMENT = "k9settings";
public static final String VERSION_ATTRIBUTE = "version";
public static final String FILE_FORMAT_ATTRIBUTE = "format";
public static final String GLOBAL_ELEMENT = "global";
public static final String SETTINGS_ELEMENT = "settings";
public static final String ACCOUNTS_ELEMENT = "accounts";
public static final String ACCOUNT_ELEMENT = "account";
public static final String UUID_ATTRIBUTE = "uuid";
public static final String INCOMING_SERVER_ELEMENT = "incoming-server";
public static final String OUTGOING_SERVER_ELEMENT = "outgoing-server";
public static final String TYPE_ATTRIBUTE = "type";
public static final String HOST_ELEMENT = "host";
public static final String PORT_ELEMENT = "port";
public static final String CONNECTION_SECURITY_ELEMENT = "connection-security";
public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type";
public static final String USERNAME_ELEMENT = "username";
public static final String CLIENT_CERTIFICATE_ALIAS_ELEMENT = "client-cert-alias";
public static final String PASSWORD_ELEMENT = "password";
public static final String EXTRA_ELEMENT = "extra";
public static final String IDENTITIES_ELEMENT = "identities";
public static final String IDENTITY_ELEMENT = "identity";
public static final String FOLDERS_ELEMENT = "folders";
public static final String FOLDER_ELEMENT = "folder";
public static final String NAME_ATTRIBUTE = "name";
public static final String VALUE_ELEMENT = "value";
public static final String KEY_ATTRIBUTE = "key";
public static final String NAME_ELEMENT = "name";
public static final String EMAIL_ELEMENT = "email";
public static final String DESCRIPTION_ELEMENT = "description";
public static String exportToFile(Context context, boolean includeGlobals,
Set accountUuids)
throws SettingsImportExportException {
OutputStream os = null;
String filename = null;
try
{
File dir = new File(Environment.getExternalStorageDirectory() + File.separator
+ context.getPackageName());
dir.mkdirs();
File file = Utility.createUniqueFile(dir, EXPORT_FILENAME);
filename = file.getAbsolutePath();
os = new FileOutputStream(filename);
exportPreferences(context, os, includeGlobals, accountUuids);
// If all went well, we return the name of the file just written.
return filename;
} catch (Exception e) {
throw new SettingsImportExportException(e);
} finally {
if (os != null) {
try {
os.close();
} catch (IOException ioe) {
Log.w(K9.LOG_TAG, "Couldn't close exported settings file: " + filename);
}
}
}
}
public static void exportPreferences(Context context, OutputStream os, boolean includeGlobals,
Set accountUuids) throws SettingsImportExportException {
try {
XmlSerializer serializer = Xml.newSerializer();
serializer.setOutput(os, "UTF-8");
serializer.startDocument(null, Boolean.TRUE);
// Output with indentation
serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
serializer.startTag(null, ROOT_ELEMENT);
serializer.attribute(null, VERSION_ATTRIBUTE, Integer.toString(Settings.VERSION));
serializer.attribute(null, FILE_FORMAT_ATTRIBUTE,
Integer.toString(FILE_FORMAT_VERSION));
Log.i(K9.LOG_TAG, "Exporting preferences");
Preferences preferences = Preferences.getPreferences(context);
SharedPreferences storage = preferences.getPreferences();
Set exportAccounts;
if (accountUuids == null) {
Account[] accounts = preferences.getAccounts();
exportAccounts = new HashSet();
for (Account account : accounts) {
exportAccounts.add(account.getUuid());
}
} else {
exportAccounts = accountUuids;
}
Map prefs = new TreeMap(storage.getAll());
if (includeGlobals) {
serializer.startTag(null, GLOBAL_ELEMENT);
writeSettings(serializer, prefs);
serializer.endTag(null, GLOBAL_ELEMENT);
}
serializer.startTag(null, ACCOUNTS_ELEMENT);
for (String accountUuid : exportAccounts) {
Account account = preferences.getAccount(accountUuid);
writeAccount(serializer, account, prefs);
}
serializer.endTag(null, ACCOUNTS_ELEMENT);
serializer.endTag(null, ROOT_ELEMENT);
serializer.endDocument();
serializer.flush();
} catch (Exception e) {
throw new SettingsImportExportException(e.getLocalizedMessage(), e);
}
}
private static void writeSettings(XmlSerializer serializer,
Map prefs) throws IOException {
for (Entry> versionedSetting :
GlobalSettings.SETTINGS.entrySet()) {
String key = versionedSetting.getKey();
String valueString = (String) prefs.get(key);
TreeMap versions = versionedSetting.getValue();
Integer highestVersion = versions.lastKey();
SettingsDescription setting = versions.get(highestVersion);
if (setting == null) {
// Setting was removed.
continue;
}
if (valueString != null) {
try {
Object value = setting.fromString(valueString);
String outputValue = setting.toPrettyString(value);
writeKeyValue(serializer, key, outputValue);
} catch (InvalidSettingValueException e) {
Log.w(K9.LOG_TAG, "Global setting \"" + key + "\" has invalid value \"" +
valueString + "\" in preference storage. This shouldn't happen!");
}
} else {
if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Couldn't find key \"" + key + "\" in preference storage." +
"Using default value.");
}
Object value = setting.getDefaultValue();
String outputValue = setting.toPrettyString(value);
writeKeyValue(serializer, key, outputValue);
}
}
}
private static void writeAccount(XmlSerializer serializer, Account account,
Map prefs) throws IOException {
Set identities = new HashSet();
Set folders = new HashSet();
String accountUuid = account.getUuid();
serializer.startTag(null, ACCOUNT_ELEMENT);
serializer.attribute(null, UUID_ATTRIBUTE, accountUuid);
String name = (String) prefs.get(accountUuid + "." + Account.ACCOUNT_DESCRIPTION_KEY);
if (name != null) {
serializer.startTag(null, NAME_ELEMENT);
serializer.text(name);
serializer.endTag(null, NAME_ELEMENT);
}
// Write incoming server settings
ServerSettings incoming = Store.decodeStoreUri(account.getStoreUri());
serializer.startTag(null, INCOMING_SERVER_ELEMENT);
serializer.attribute(null, TYPE_ATTRIBUTE, incoming.type);
writeElement(serializer, HOST_ELEMENT, incoming.host);
if (incoming.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
}
if (incoming.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
}
if (incoming.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, incoming.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, incoming.clientCertificateAlias);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
Map extras = incoming.getExtra();
if (extras != null && extras.size() > 0) {
serializer.startTag(null, EXTRA_ELEMENT);
for (Entry extra : extras.entrySet()) {
writeKeyValue(serializer, extra.getKey(), extra.getValue());
}
serializer.endTag(null, EXTRA_ELEMENT);
}
serializer.endTag(null, INCOMING_SERVER_ELEMENT);
// Write outgoing server settings
ServerSettings outgoing = Transport.decodeTransportUri(account.getTransportUri());
serializer.startTag(null, OUTGOING_SERVER_ELEMENT);
serializer.attribute(null, TYPE_ATTRIBUTE, outgoing.type);
writeElement(serializer, HOST_ELEMENT, outgoing.host);
if (outgoing.port != -1) {
writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
}
if (outgoing.connectionSecurity != null) {
writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
}
if (outgoing.authenticationType != null) {
writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType.name());
}
writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
writeElement(serializer, CLIENT_CERTIFICATE_ALIAS_ELEMENT, outgoing.clientCertificateAlias);
// XXX For now we don't export the password
//writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);
extras = outgoing.getExtra();
if (extras != null && extras.size() > 0) {
serializer.startTag(null, EXTRA_ELEMENT);
for (Entry extra : extras.entrySet()) {
writeKeyValue(serializer, extra.getKey(), extra.getValue());
}
serializer.endTag(null, EXTRA_ELEMENT);
}
serializer.endTag(null, OUTGOING_SERVER_ELEMENT);
// Write account settings
serializer.startTag(null, SETTINGS_ELEMENT);
for (Map.Entry entry : prefs.entrySet()) {
String key = entry.getKey();
String valueString = entry.getValue().toString();
String[] comps = key.split("\\.");
if (comps.length < 2) {
// Skip global settings
continue;
}
String keyUuid = comps[0];
String secondPart = comps[1];
if (!keyUuid.equals(accountUuid)) {
// Setting doesn't belong to the account we're currently writing.
continue;
}
String keyPart;
if (comps.length >= 3) {
String thirdPart = comps[2];
if (Account.IDENTITY_DESCRIPTION_KEY.equals(secondPart)) {
// This is an identity key. Save identity index for later...
try {
identities.add(Integer.parseInt(thirdPart));
} catch (NumberFormatException e) { /* ignore */ }
// ... but don't write it now.
continue;
}
if (FolderSettings.SETTINGS.containsKey(thirdPart)) {
// This is a folder key. Save folder name for later...
folders.add(secondPart);
// ... but don't write it now.
continue;
}
// Strip account UUID from key
keyPart = key.substring(comps[0].length() + 1);
} else {
keyPart = secondPart;
}
TreeMap versionedSetting =
AccountSettings.SETTINGS.get(keyPart);
if (versionedSetting != null) {
Integer highestVersion = versionedSetting.lastKey();
SettingsDescription setting = versionedSetting.get(highestVersion);
if (setting != null) {
// Only export account settings that can be found in AccountSettings.SETTINGS
try {
Object value = setting.fromString(valueString);
String pretty = setting.toPrettyString(value);
writeKeyValue(serializer, keyPart, pretty);
} catch (InvalidSettingValueException e) {
Log.w(K9.LOG_TAG, "Account setting \"" + keyPart + "\" (" +
account.getDescription() + ") has invalid value \"" + valueString +
"\" in preference storage. This shouldn't happen!");
}
}
}
}
serializer.endTag(null, SETTINGS_ELEMENT);
if (identities.size() > 0) {
serializer.startTag(null, IDENTITIES_ELEMENT);
// Sort identity indices (that's why we store them as Integers)
List sortedIdentities = new ArrayList(identities);
Collections.sort(sortedIdentities);
for (Integer identityIndex : sortedIdentities) {
writeIdentity(serializer, accountUuid, identityIndex.toString(), prefs);
}
serializer.endTag(null, IDENTITIES_ELEMENT);
}
if (folders.size() > 0) {
serializer.startTag(null, FOLDERS_ELEMENT);
for (String folder : folders) {
writeFolder(serializer, accountUuid, folder, prefs);
}
serializer.endTag(null, FOLDERS_ELEMENT);
}
serializer.endTag(null, ACCOUNT_ELEMENT);
}
private static void writeIdentity(XmlSerializer serializer, String accountUuid,
String identity, Map prefs) throws IOException {
serializer.startTag(null, IDENTITY_ELEMENT);
String prefix = accountUuid + ".";
String suffix = "." + identity;
// Write name belonging to the identity
String name = (String) prefs.get(prefix + Account.IDENTITY_NAME_KEY + suffix);
serializer.startTag(null, NAME_ELEMENT);
serializer.text(name);
serializer.endTag(null, NAME_ELEMENT);
// Write email address belonging to the identity
String email = (String) prefs.get(prefix + Account.IDENTITY_EMAIL_KEY + suffix);
serializer.startTag(null, EMAIL_ELEMENT);
serializer.text(email);
serializer.endTag(null, EMAIL_ELEMENT);
// Write identity description
String description = (String) prefs.get(prefix + Account.IDENTITY_DESCRIPTION_KEY + suffix);
if (description != null) {
serializer.startTag(null, DESCRIPTION_ELEMENT);
serializer.text(description);
serializer.endTag(null, DESCRIPTION_ELEMENT);
}
// Write identity settings
serializer.startTag(null, SETTINGS_ELEMENT);
for (Map.Entry entry : prefs.entrySet()) {
String key = entry.getKey();
String valueString = entry.getValue().toString();
String[] comps = key.split("\\.");
if (comps.length < 3) {
// Skip non-identity config entries
continue;
}
String keyUuid = comps[0];
String identityKey = comps[1];
String identityIndex = comps[2];
if (!keyUuid.equals(accountUuid) || !identityIndex.equals(identity)) {
// Skip entries that belong to another identity
continue;
}
TreeMap versionedSetting =
IdentitySettings.SETTINGS.get(identityKey);
if (versionedSetting != null) {
Integer highestVersion = versionedSetting.lastKey();
SettingsDescription setting = versionedSetting.get(highestVersion);
if (setting != null) {
// Only write settings that have an entry in IdentitySettings.SETTINGS
try {
Object value = setting.fromString(valueString);
String outputValue = setting.toPrettyString(value);
writeKeyValue(serializer, identityKey, outputValue);
} catch (InvalidSettingValueException e) {
Log.w(K9.LOG_TAG, "Identity setting \"" + identityKey +
"\" has invalid value \"" + valueString +
"\" in preference storage. This shouldn't happen!");
}
}
}
}
serializer.endTag(null, SETTINGS_ELEMENT);
serializer.endTag(null, IDENTITY_ELEMENT);
}
private static void writeFolder(XmlSerializer serializer, String accountUuid,
String folder, Map prefs) throws IOException {
serializer.startTag(null, FOLDER_ELEMENT);
serializer.attribute(null, NAME_ATTRIBUTE, folder);
// Write folder settings
for (Map.Entry entry : prefs.entrySet()) {
String key = entry.getKey();
String valueString = entry.getValue().toString();
String[] comps = key.split("\\.");
if (comps.length < 3) {
// Skip non-folder config entries
continue;
}
String keyUuid = comps[0];
String folderName = comps[1];
String folderKey = comps[2];
if (!keyUuid.equals(accountUuid) || !folderName.equals(folder)) {
// Skip entries that belong to another folder
continue;
}
TreeMap versionedSetting =
FolderSettings.SETTINGS.get(folderKey);
if (versionedSetting != null) {
Integer highestVersion = versionedSetting.lastKey();
SettingsDescription setting = versionedSetting.get(highestVersion);
if (setting != null) {
// Only write settings that have an entry in FolderSettings.SETTINGS
try {
Object value = setting.fromString(valueString);
String outputValue = setting.toPrettyString(value);
writeKeyValue(serializer, folderKey, outputValue);
} catch (InvalidSettingValueException e) {
Log.w(K9.LOG_TAG, "Folder setting \"" + folderKey +
"\" has invalid value \"" + valueString +
"\" in preference storage. This shouldn't happen!");
}
}
}
}
serializer.endTag(null, FOLDER_ELEMENT);
}
private static void writeElement(XmlSerializer serializer, String elementName, String value)
throws IllegalArgumentException, IllegalStateException, IOException {
if (value != null) {
serializer.startTag(null, elementName);
serializer.text(value);
serializer.endTag(null, elementName);
}
}
private static void writeKeyValue(XmlSerializer serializer, String key, String value)
throws IllegalArgumentException, IllegalStateException, IOException {
serializer.startTag(null, VALUE_ELEMENT);
serializer.attribute(null, KEY_ATTRIBUTE, key);
if (value != null) {
serializer.text(value);
}
serializer.endTag(null, VALUE_ELEMENT);
}
}