mirror of
https://github.com/moparisthebest/k-9
synced 2025-01-08 04:08:15 -05:00
0fac8e999d
Remove home-grown date formatting, and replace it by usage of the DateUtils class which is present since API level 3.
1157 lines
47 KiB
Java
1157 lines
47 KiB
Java
package com.fsck.k9.preferences;
|
|
|
|
import java.io.IOException;
|
|
import java.io.InputStream;
|
|
import java.io.InputStreamReader;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.UUID;
|
|
|
|
import org.xmlpull.v1.XmlPullParser;
|
|
import org.xmlpull.v1.XmlPullParserException;
|
|
import org.xmlpull.v1.XmlPullParserFactory;
|
|
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
import android.util.Log;
|
|
|
|
import com.fsck.k9.Account;
|
|
import com.fsck.k9.Identity;
|
|
import com.fsck.k9.K9;
|
|
import com.fsck.k9.Preferences;
|
|
import com.fsck.k9.helper.Utility;
|
|
import com.fsck.k9.mail.ConnectionSecurity;
|
|
import com.fsck.k9.mail.ServerSettings;
|
|
import com.fsck.k9.mail.Store;
|
|
import com.fsck.k9.mail.Transport;
|
|
import com.fsck.k9.mail.store.WebDavStore;
|
|
import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
|
|
|
|
public class SettingsImporter {
|
|
|
|
/**
|
|
* Class to list the contents of an import file/stream.
|
|
*
|
|
* @see SettingsImporter#getImportStreamContents(InputStream)
|
|
*/
|
|
public static class ImportContents {
|
|
/**
|
|
* True, if the import file contains global settings.
|
|
*/
|
|
public final boolean globalSettings;
|
|
|
|
/**
|
|
* The list of accounts found in the import file. Never {@code null}.
|
|
*/
|
|
public final List<AccountDescription> accounts;
|
|
|
|
private ImportContents(boolean globalSettings, List<AccountDescription> accounts) {
|
|
this.globalSettings = globalSettings;
|
|
this.accounts = accounts;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Class to describe an account (name, UUID).
|
|
*
|
|
* @see ImportContents
|
|
*/
|
|
public static class AccountDescription {
|
|
/**
|
|
* The name of the account.
|
|
*/
|
|
public final String name;
|
|
|
|
/**
|
|
* The UUID of the account.
|
|
*/
|
|
public final String uuid;
|
|
|
|
private AccountDescription(String name, String uuid) {
|
|
this.name = name;
|
|
this.uuid = uuid;
|
|
}
|
|
}
|
|
|
|
public static class AccountDescriptionPair {
|
|
public final AccountDescription original;
|
|
public final AccountDescription imported;
|
|
public final boolean overwritten;
|
|
|
|
private AccountDescriptionPair(AccountDescription original, AccountDescription imported,
|
|
boolean overwritten) {
|
|
this.original = original;
|
|
this.imported = imported;
|
|
this.overwritten = overwritten;
|
|
}
|
|
}
|
|
|
|
public static class ImportResults {
|
|
public final boolean globalSettings;
|
|
public final List<AccountDescriptionPair> importedAccounts;
|
|
public final List<AccountDescription> errorneousAccounts;
|
|
|
|
private ImportResults(boolean globalSettings,
|
|
List<AccountDescriptionPair> importedAccounts,
|
|
List<AccountDescription> errorneousAccounts) {
|
|
this.globalSettings = globalSettings;
|
|
this.importedAccounts = importedAccounts;
|
|
this.errorneousAccounts = errorneousAccounts;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses an import {@link InputStream} and returns information on whether it contains global
|
|
* settings and/or account settings. For all account configurations found, the name of the
|
|
* account along with the account UUID is returned.
|
|
*
|
|
* @param inputStream
|
|
* An {@code InputStream} to read the settings from.
|
|
*
|
|
* @return An {@link ImportContents} instance containing information about the contents of the
|
|
* settings file.
|
|
*
|
|
* @throws SettingsImportExportException
|
|
* In case of an error.
|
|
*/
|
|
public static ImportContents getImportStreamContents(InputStream inputStream)
|
|
throws SettingsImportExportException {
|
|
|
|
try {
|
|
// Parse the import stream but don't save individual settings (overview=true)
|
|
Imported imported = parseSettings(inputStream, false, null, true);
|
|
|
|
// If the stream contains global settings the "globalSettings" member will not be null
|
|
boolean globalSettings = (imported.globalSettings != null);
|
|
|
|
final List<AccountDescription> accounts = new ArrayList<AccountDescription>();
|
|
// If the stream contains at least one account configuration the "accounts" member
|
|
// will not be null.
|
|
if (imported.accounts != null) {
|
|
for (ImportedAccount account : imported.accounts.values()) {
|
|
accounts.add(new AccountDescription(account.name, account.uuid));
|
|
}
|
|
}
|
|
|
|
//TODO: throw exception if neither global settings nor account settings could be found
|
|
|
|
return new ImportContents(globalSettings, accounts);
|
|
|
|
} catch (SettingsImportExportException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
throw new SettingsImportExportException(e);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reads an import {@link InputStream} and imports the global settings and/or account
|
|
* configurations specified by the arguments.
|
|
*
|
|
* @param context
|
|
* A {@link Context} instance.
|
|
* @param inputStream
|
|
* The {@code InputStream} to read the settings from.
|
|
* @param globalSettings
|
|
* {@code true} if global settings should be imported from the file.
|
|
* @param accountUuids
|
|
* A list of UUIDs of the accounts that should be imported.
|
|
* @param overwrite
|
|
* {@code true} if existing accounts should be overwritten when an account with the
|
|
* same UUID is found in the settings file.<br>
|
|
* <strong>Note:</strong> This can have side-effects we currently don't handle, e.g.
|
|
* changing the account type from IMAP to POP3. So don't use this for now!
|
|
*
|
|
* @return An {@link ImportResults} instance containing information about errors and
|
|
* successfully imported accounts.
|
|
*
|
|
* @throws SettingsImportExportException
|
|
* In case of an error.
|
|
*/
|
|
public static ImportResults importSettings(Context context, InputStream inputStream,
|
|
boolean globalSettings, List<String> accountUuids, boolean overwrite)
|
|
throws SettingsImportExportException {
|
|
|
|
try
|
|
{
|
|
boolean globalSettingsImported = false;
|
|
List<AccountDescriptionPair> importedAccounts = new ArrayList<AccountDescriptionPair>();
|
|
List<AccountDescription> errorneousAccounts = new ArrayList<AccountDescription>();
|
|
|
|
Imported imported = parseSettings(inputStream, globalSettings, accountUuids, false);
|
|
|
|
Preferences preferences = Preferences.getPreferences(context);
|
|
SharedPreferences storage = preferences.getPreferences();
|
|
|
|
if (globalSettings) {
|
|
try {
|
|
SharedPreferences.Editor editor = storage.edit();
|
|
if (imported.globalSettings != null) {
|
|
importGlobalSettings(storage, editor, imported.contentVersion,
|
|
imported.globalSettings);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Was asked to import global settings but none found.");
|
|
}
|
|
if (editor.commit()) {
|
|
if (K9.DEBUG) {
|
|
Log.v(K9.LOG_TAG, "Committed global settings to the preference " +
|
|
"storage.");
|
|
}
|
|
globalSettingsImported = true;
|
|
} else {
|
|
if (K9.DEBUG) {
|
|
Log.v(K9.LOG_TAG, "Failed to commit global settings to the " +
|
|
"preference storage");
|
|
}
|
|
}
|
|
} catch (Exception e) {
|
|
Log.e(K9.LOG_TAG, "Exception while importing global settings", e);
|
|
}
|
|
}
|
|
|
|
if (accountUuids != null && accountUuids.size() > 0) {
|
|
if (imported.accounts != null) {
|
|
for (String accountUuid : accountUuids) {
|
|
if (imported.accounts.containsKey(accountUuid)) {
|
|
ImportedAccount account = imported.accounts.get(accountUuid);
|
|
try {
|
|
SharedPreferences.Editor editor = storage.edit();
|
|
|
|
AccountDescriptionPair importResult = importAccount(context,
|
|
editor, imported.contentVersion, account, overwrite);
|
|
|
|
if (editor.commit()) {
|
|
if (K9.DEBUG) {
|
|
Log.v(K9.LOG_TAG, "Committed settings for account \"" +
|
|
importResult.imported.name +
|
|
"\" to the settings database.");
|
|
}
|
|
|
|
// Add UUID of the account we just imported to the list of
|
|
// account UUIDs
|
|
if (!importResult.overwritten) {
|
|
editor = storage.edit();
|
|
|
|
String newUuid = importResult.imported.uuid;
|
|
String oldAccountUuids = storage.getString("accountUuids", "");
|
|
String newAccountUuids = (oldAccountUuids.length() > 0) ?
|
|
oldAccountUuids + "," + newUuid : newUuid;
|
|
|
|
putString(editor, "accountUuids", newAccountUuids);
|
|
|
|
if (!editor.commit()) {
|
|
throw new SettingsImportExportException("Failed to set account UUID list");
|
|
}
|
|
}
|
|
|
|
// Reload accounts
|
|
preferences.loadAccounts();
|
|
|
|
importedAccounts.add(importResult);
|
|
} else {
|
|
if (K9.DEBUG) {
|
|
Log.w(K9.LOG_TAG, "Error while committing settings for " +
|
|
"account \"" + importResult.original.name +
|
|
"\" to the settings database.");
|
|
}
|
|
errorneousAccounts.add(importResult.original);
|
|
}
|
|
} catch (InvalidSettingValueException e) {
|
|
if (K9.DEBUG) {
|
|
Log.e(K9.LOG_TAG, "Encountered invalid setting while " +
|
|
"importing account \"" + account.name + "\"", e);
|
|
}
|
|
errorneousAccounts.add(new AccountDescription(account.name, account.uuid));
|
|
} catch (Exception e) {
|
|
Log.e(K9.LOG_TAG, "Exception while importing account \"" +
|
|
account.name + "\"", e);
|
|
errorneousAccounts.add(new AccountDescription(account.name, account.uuid));
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Was asked to import account with UUID " +
|
|
accountUuid + ". But this account wasn't found.");
|
|
}
|
|
}
|
|
|
|
SharedPreferences.Editor editor = storage.edit();
|
|
|
|
String defaultAccountUuid = storage.getString("defaultAccountUuid", null);
|
|
if (defaultAccountUuid == null) {
|
|
putString(editor, "defaultAccountUuid", accountUuids.get(0));
|
|
}
|
|
|
|
if (!editor.commit()) {
|
|
throw new SettingsImportExportException("Failed to set default account");
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Was asked to import at least one account but none found.");
|
|
}
|
|
}
|
|
|
|
preferences.loadAccounts();
|
|
K9.loadPrefs(preferences);
|
|
K9.setServicesEnabled(context);
|
|
|
|
return new ImportResults(globalSettingsImported, importedAccounts, errorneousAccounts);
|
|
|
|
} catch (SettingsImportExportException e) {
|
|
throw e;
|
|
} catch (Exception e) {
|
|
throw new SettingsImportExportException(e);
|
|
}
|
|
}
|
|
|
|
private static void importGlobalSettings(SharedPreferences storage,
|
|
SharedPreferences.Editor editor, int contentVersion, ImportedSettings settings) {
|
|
|
|
// Validate global settings
|
|
Map<String, Object> validatedSettings = GlobalSettings.validate(contentVersion,
|
|
settings.settings);
|
|
|
|
// Upgrade global settings to current content version
|
|
if (contentVersion != Settings.VERSION) {
|
|
GlobalSettings.upgrade(contentVersion, validatedSettings);
|
|
}
|
|
|
|
// Convert global settings to the string representation used in preference storage
|
|
Map<String, String> stringSettings = GlobalSettings.convert(validatedSettings);
|
|
|
|
// Use current global settings as base and overwrite with validated settings read from the
|
|
// import file.
|
|
Map<String, String> mergedSettings =
|
|
new HashMap<String, String>(GlobalSettings.getGlobalSettings(storage));
|
|
mergedSettings.putAll(stringSettings);
|
|
|
|
for (Map.Entry<String, String> setting : mergedSettings.entrySet()) {
|
|
String key = setting.getKey();
|
|
String value = setting.getValue();
|
|
putString(editor, key, value);
|
|
}
|
|
}
|
|
|
|
private static AccountDescriptionPair importAccount(Context context,
|
|
SharedPreferences.Editor editor, int contentVersion, ImportedAccount account,
|
|
boolean overwrite) throws InvalidSettingValueException {
|
|
|
|
AccountDescription original = new AccountDescription(account.name, account.uuid);
|
|
|
|
Preferences prefs = Preferences.getPreferences(context);
|
|
Account[] accounts = prefs.getAccounts();
|
|
|
|
String uuid = account.uuid;
|
|
Account existingAccount = prefs.getAccount(uuid);
|
|
boolean mergeImportedAccount = (overwrite && existingAccount != null);
|
|
|
|
if (!overwrite && existingAccount != null) {
|
|
// An account with this UUID already exists, but we're not allowed to overwrite it.
|
|
// So generate a new UUID.
|
|
uuid = UUID.randomUUID().toString();
|
|
}
|
|
|
|
// Make sure the account name is unique
|
|
String accountName = account.name;
|
|
if (isAccountNameUsed(accountName, accounts)) {
|
|
// Account name is already in use. So generate a new one by appending " (x)", where x
|
|
// is the first number >= 1 that results in an unused account name.
|
|
for (int i = 1; i <= accounts.length; i++) {
|
|
accountName = account.name + " (" + i + ")";
|
|
if (!isAccountNameUsed(accountName, accounts)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write account name
|
|
String accountKeyPrefix = uuid + ".";
|
|
putString(editor, accountKeyPrefix + Account.ACCOUNT_DESCRIPTION_KEY, accountName);
|
|
|
|
if (account.incoming == null) {
|
|
// We don't import accounts without incoming server settings
|
|
throw new InvalidSettingValueException();
|
|
}
|
|
|
|
// Write incoming server settings (storeUri)
|
|
ServerSettings incoming = new ImportedServerSettings(account.incoming);
|
|
String storeUri = Store.createStoreUri(incoming);
|
|
putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri));
|
|
|
|
// Mark account as disabled if the settings file didn't contain a password
|
|
boolean createAccountDisabled = (incoming.password == null ||
|
|
incoming.password.length() == 0);
|
|
|
|
if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) {
|
|
// All account types except WebDAV need to provide outgoing server settings
|
|
throw new InvalidSettingValueException();
|
|
}
|
|
|
|
if (account.outgoing != null) {
|
|
// Write outgoing server settings (transportUri)
|
|
ServerSettings outgoing = new ImportedServerSettings(account.outgoing);
|
|
String transportUri = Transport.createTransportUri(outgoing);
|
|
putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri));
|
|
|
|
// Mark account as disabled if the settings file didn't contain a password
|
|
if (outgoing.password == null || outgoing.password.length() == 0) {
|
|
createAccountDisabled = true;
|
|
}
|
|
}
|
|
|
|
// Write key to mark account as disabled if necessary
|
|
if (createAccountDisabled) {
|
|
editor.putBoolean(accountKeyPrefix + "enabled", false);
|
|
}
|
|
|
|
// Validate account settings
|
|
Map<String, Object> validatedSettings =
|
|
AccountSettings.validate(contentVersion, account.settings.settings,
|
|
!mergeImportedAccount);
|
|
|
|
// Upgrade account settings to current content version
|
|
if (contentVersion != Settings.VERSION) {
|
|
AccountSettings.upgrade(contentVersion, validatedSettings);
|
|
}
|
|
|
|
// Convert account settings to the string representation used in preference storage
|
|
Map<String, String> stringSettings = AccountSettings.convert(validatedSettings);
|
|
|
|
// Merge account settings if necessary
|
|
Map<String, String> writeSettings;
|
|
if (mergeImportedAccount) {
|
|
writeSettings = new HashMap<String, String>(
|
|
AccountSettings.getAccountSettings(prefs.getPreferences(), uuid));
|
|
writeSettings.putAll(stringSettings);
|
|
} else {
|
|
writeSettings = stringSettings;
|
|
}
|
|
|
|
// Write account settings
|
|
for (Map.Entry<String, String> setting : writeSettings.entrySet()) {
|
|
String key = accountKeyPrefix + setting.getKey();
|
|
String value = setting.getValue();
|
|
putString(editor, key, value);
|
|
}
|
|
|
|
// If it's a new account generate and write a new "accountNumber"
|
|
if (!mergeImportedAccount) {
|
|
int newAccountNumber = Account.generateAccountNumber(prefs);
|
|
putString(editor, accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber));
|
|
}
|
|
|
|
// Write identities
|
|
if (account.identities != null) {
|
|
importIdentities(editor, contentVersion, uuid, account, overwrite, existingAccount,
|
|
prefs);
|
|
} else if (!mergeImportedAccount) {
|
|
// Require accounts to at least have one identity
|
|
throw new InvalidSettingValueException();
|
|
}
|
|
|
|
// Write folder settings
|
|
if (account.folders != null) {
|
|
for (ImportedFolder folder : account.folders) {
|
|
importFolder(editor, contentVersion, uuid, folder, mergeImportedAccount, prefs);
|
|
}
|
|
}
|
|
|
|
//TODO: sync folder settings with localstore?
|
|
|
|
AccountDescription imported = new AccountDescription(accountName, uuid);
|
|
return new AccountDescriptionPair(original, imported, mergeImportedAccount);
|
|
}
|
|
|
|
private static void importFolder(SharedPreferences.Editor editor, int contentVersion,
|
|
String uuid, ImportedFolder folder, boolean overwrite, Preferences prefs) {
|
|
|
|
// Validate folder settings
|
|
Map<String, Object> validatedSettings =
|
|
FolderSettings.validate(contentVersion, folder.settings.settings, !overwrite);
|
|
|
|
// Upgrade folder settings to current content version
|
|
if (contentVersion != Settings.VERSION) {
|
|
FolderSettings.upgrade(contentVersion, validatedSettings);
|
|
}
|
|
|
|
// Convert folder settings to the string representation used in preference storage
|
|
Map<String, String> stringSettings = FolderSettings.convert(validatedSettings);
|
|
|
|
// Merge folder settings if necessary
|
|
Map<String, String> writeSettings;
|
|
if (overwrite) {
|
|
writeSettings = FolderSettings.getFolderSettings(prefs.getPreferences(),
|
|
uuid, folder.name);
|
|
writeSettings.putAll(stringSettings);
|
|
} else {
|
|
writeSettings = stringSettings;
|
|
}
|
|
|
|
// Write folder settings
|
|
String prefix = uuid + "." + folder.name + ".";
|
|
for (Map.Entry<String, String> setting : writeSettings.entrySet()) {
|
|
String key = prefix + setting.getKey();
|
|
String value = setting.getValue();
|
|
putString(editor, key, value);
|
|
}
|
|
}
|
|
|
|
private static void importIdentities(SharedPreferences.Editor editor, int contentVersion,
|
|
String uuid, ImportedAccount account, boolean overwrite, Account existingAccount,
|
|
Preferences prefs) throws InvalidSettingValueException {
|
|
|
|
String accountKeyPrefix = uuid + ".";
|
|
|
|
// Gather information about existing identities for this account (if any)
|
|
int nextIdentityIndex = 0;
|
|
final List<Identity> existingIdentities;
|
|
if (overwrite && existingAccount != null) {
|
|
existingIdentities = existingAccount.getIdentities();
|
|
nextIdentityIndex = existingIdentities.size();
|
|
} else {
|
|
existingIdentities = new ArrayList<Identity>();
|
|
}
|
|
|
|
// Write identities
|
|
for (ImportedIdentity identity : account.identities) {
|
|
int writeIdentityIndex = nextIdentityIndex;
|
|
boolean mergeSettings = false;
|
|
if (overwrite && existingIdentities.size() > 0) {
|
|
int identityIndex = findIdentity(identity, existingIdentities);
|
|
if (identityIndex != -1) {
|
|
writeIdentityIndex = identityIndex;
|
|
mergeSettings = true;
|
|
}
|
|
}
|
|
if (!mergeSettings) {
|
|
nextIdentityIndex++;
|
|
}
|
|
|
|
String identityDescription = (identity.description == null) ?
|
|
"Imported" : identity.description;
|
|
if (isIdentityDescriptionUsed(identityDescription, existingIdentities)) {
|
|
// Identity description is already in use. So generate a new one by appending
|
|
// " (x)", where x is the first number >= 1 that results in an unused identity
|
|
// description.
|
|
for (int i = 1; i <= existingIdentities.size(); i++) {
|
|
identityDescription = identity.description + " (" + i + ")";
|
|
if (!isIdentityDescriptionUsed(identityDescription, existingIdentities)) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
String identitySuffix = "." + writeIdentityIndex;
|
|
|
|
// Write name used in identity
|
|
String identityName = (identity.name == null) ? "" : identity.name;
|
|
putString(editor, accountKeyPrefix + Account.IDENTITY_NAME_KEY + identitySuffix,
|
|
identityName);
|
|
|
|
// Validate email address
|
|
if (!IdentitySettings.isEmailAddressValid(identity.email)) {
|
|
throw new InvalidSettingValueException();
|
|
}
|
|
|
|
// Write email address
|
|
putString(editor, accountKeyPrefix + Account.IDENTITY_EMAIL_KEY + identitySuffix,
|
|
identity.email);
|
|
|
|
// Write identity description
|
|
putString(editor, accountKeyPrefix + Account.IDENTITY_DESCRIPTION_KEY + identitySuffix,
|
|
identityDescription);
|
|
|
|
if (identity.settings != null) {
|
|
// Validate identity settings
|
|
Map<String, Object> validatedSettings = IdentitySettings.validate(
|
|
contentVersion, identity.settings.settings, !mergeSettings);
|
|
|
|
// Upgrade identity settings to current content version
|
|
if (contentVersion != Settings.VERSION) {
|
|
IdentitySettings.upgrade(contentVersion, validatedSettings);
|
|
}
|
|
|
|
// Convert identity settings to the representation used in preference storage
|
|
Map<String, String> stringSettings = IdentitySettings.convert(validatedSettings);
|
|
|
|
// Merge identity settings if necessary
|
|
Map<String, String> writeSettings;
|
|
if (mergeSettings) {
|
|
writeSettings = new HashMap<String, String>(IdentitySettings.getIdentitySettings(
|
|
prefs.getPreferences(), uuid, writeIdentityIndex));
|
|
writeSettings.putAll(stringSettings);
|
|
} else {
|
|
writeSettings = stringSettings;
|
|
}
|
|
|
|
// Write identity settings
|
|
for (Map.Entry<String, String> setting : writeSettings.entrySet()) {
|
|
String key = accountKeyPrefix + setting.getKey() + identitySuffix;
|
|
String value = setting.getValue();
|
|
putString(editor, key, value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static boolean isAccountNameUsed(String name, Account[] accounts) {
|
|
for (Account account : accounts) {
|
|
if (account.getDescription().equals(name)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static boolean isIdentityDescriptionUsed(String description, List<Identity> identities) {
|
|
for (Identity identitiy : identities) {
|
|
if (identitiy.getDescription().equals(description)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static int findIdentity(ImportedIdentity identity,
|
|
List<Identity> identities) {
|
|
for (int i = 0; i < identities.size(); i++) {
|
|
Identity existingIdentity = identities.get(i);
|
|
if (existingIdentity.getName().equals(identity.name) &&
|
|
existingIdentity.getEmail().equals(identity.email)) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Write to an {@link SharedPreferences.Editor} while logging what is written if debug logging
|
|
* is enabled.
|
|
*
|
|
* @param editor
|
|
* The {@code Editor} to write to.
|
|
* @param key
|
|
* The name of the preference to modify.
|
|
* @param value
|
|
* The new value for the preference.
|
|
*/
|
|
private static void putString(SharedPreferences.Editor editor, String key, String value) {
|
|
if (K9.DEBUG) {
|
|
String outputValue = value;
|
|
if (!K9.DEBUG_SENSITIVE &&
|
|
(key.endsWith(".transportUri") || key.endsWith(".storeUri"))) {
|
|
outputValue = "*sensitive*";
|
|
}
|
|
Log.v(K9.LOG_TAG, "Setting " + key + "=" + outputValue);
|
|
}
|
|
editor.putString(key, value);
|
|
}
|
|
|
|
private static Imported parseSettings(InputStream inputStream, boolean globalSettings,
|
|
List<String> accountUuids, boolean overview)
|
|
throws SettingsImportExportException {
|
|
|
|
if (!overview && accountUuids == null) {
|
|
throw new IllegalArgumentException("Argument 'accountUuids' must not be null.");
|
|
}
|
|
|
|
try {
|
|
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
|
//factory.setNamespaceAware(true);
|
|
XmlPullParser xpp = factory.newPullParser();
|
|
|
|
InputStreamReader reader = new InputStreamReader(inputStream);
|
|
xpp.setInput(reader);
|
|
|
|
Imported imported = null;
|
|
int eventType = xpp.getEventType();
|
|
while (eventType != XmlPullParser.END_DOCUMENT) {
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
if (SettingsExporter.ROOT_ELEMENT.equals(xpp.getName())) {
|
|
imported = parseRoot(xpp, globalSettings, accountUuids, overview);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
if (imported == null || (overview && imported.globalSettings == null &&
|
|
imported.accounts == null)) {
|
|
throw new SettingsImportExportException("Invalid import data");
|
|
}
|
|
|
|
return imported;
|
|
} catch (Exception e) {
|
|
throw new SettingsImportExportException(e);
|
|
}
|
|
}
|
|
|
|
private static void skipToEndTag(XmlPullParser xpp, String endTag)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
|
|
eventType = xpp.next();
|
|
}
|
|
}
|
|
|
|
private static String getText(XmlPullParser xpp)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
int eventType = xpp.next();
|
|
if (eventType != XmlPullParser.TEXT) {
|
|
return "";
|
|
}
|
|
return xpp.getText();
|
|
}
|
|
|
|
private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings,
|
|
List<String> accountUuids, boolean overview)
|
|
throws XmlPullParserException, IOException, SettingsImportExportException {
|
|
|
|
Imported result = new Imported();
|
|
|
|
String fileFormatVersionString = xpp.getAttributeValue(null,
|
|
SettingsExporter.FILE_FORMAT_ATTRIBUTE);
|
|
validateFileFormatVersion(fileFormatVersionString);
|
|
|
|
String contentVersionString = xpp.getAttributeValue(null,
|
|
SettingsExporter.VERSION_ATTRIBUTE);
|
|
result.contentVersion = validateContentVersion(contentVersionString);
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.ROOT_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.GLOBAL_ELEMENT.equals(element)) {
|
|
if (overview || globalSettings) {
|
|
if (result.globalSettings == null) {
|
|
if (overview) {
|
|
result.globalSettings = new ImportedSettings();
|
|
skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
|
|
} else {
|
|
result.globalSettings = parseSettings(xpp, SettingsExporter.GLOBAL_ELEMENT);
|
|
}
|
|
} else {
|
|
skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
|
|
Log.w(K9.LOG_TAG, "More than one global settings element. Only using the first one!");
|
|
}
|
|
} else {
|
|
skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
|
|
Log.i(K9.LOG_TAG, "Skipping global settings");
|
|
}
|
|
} else if (SettingsExporter.ACCOUNTS_ELEMENT.equals(element)) {
|
|
if (result.accounts == null) {
|
|
result.accounts = parseAccounts(xpp, accountUuids, overview);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "More than one accounts element. Only using the first one!");
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static int validateFileFormatVersion(String versionString)
|
|
throws SettingsImportExportException {
|
|
|
|
if (versionString == null) {
|
|
throw new SettingsImportExportException("Missing file format version");
|
|
}
|
|
|
|
int version;
|
|
try {
|
|
version = Integer.parseInt(versionString);
|
|
} catch (NumberFormatException e) {
|
|
throw new SettingsImportExportException("Invalid file format version: " +
|
|
versionString);
|
|
}
|
|
|
|
if (version != SettingsExporter.FILE_FORMAT_VERSION) {
|
|
throw new SettingsImportExportException("Unsupported file format version: " +
|
|
versionString);
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
private static int validateContentVersion(String versionString)
|
|
throws SettingsImportExportException {
|
|
|
|
if (versionString == null) {
|
|
throw new SettingsImportExportException("Missing content version");
|
|
}
|
|
|
|
int version;
|
|
try {
|
|
version = Integer.parseInt(versionString);
|
|
} catch (NumberFormatException e) {
|
|
throw new SettingsImportExportException("Invalid content version: " +
|
|
versionString);
|
|
}
|
|
|
|
if (version < 1 || version > Settings.VERSION) {
|
|
throw new SettingsImportExportException("Unsupported content version: " +
|
|
versionString);
|
|
}
|
|
|
|
return version;
|
|
}
|
|
|
|
private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
ImportedSettings result = null;
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.VALUE_ELEMENT.equals(element)) {
|
|
String key = xpp.getAttributeValue(null, SettingsExporter.KEY_ATTRIBUTE);
|
|
String value = getText(xpp);
|
|
|
|
if (result == null) {
|
|
result = new ImportedSettings();
|
|
}
|
|
|
|
if (result.settings.containsKey(key)) {
|
|
Log.w(K9.LOG_TAG, "Already read key \"" + key + "\". Ignoring value \"" + value + "\"");
|
|
} else {
|
|
result.settings.put(key, value);
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static Map<String, ImportedAccount> parseAccounts(XmlPullParser xpp,
|
|
List<String> accountUuids, boolean overview)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
Map<String, ImportedAccount> accounts = null;
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.ACCOUNTS_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.ACCOUNT_ELEMENT.equals(element)) {
|
|
if (accounts == null) {
|
|
accounts = new HashMap<String, ImportedAccount>();
|
|
}
|
|
|
|
ImportedAccount account = parseAccount(xpp, accountUuids, overview);
|
|
|
|
if (account == null) {
|
|
// Do nothing - parseAccount() already logged a message
|
|
} else if (!accounts.containsKey(account.uuid)) {
|
|
accounts.put(account.uuid, account);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Duplicate account entries with UUID " + account.uuid +
|
|
". Ignoring!");
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return accounts;
|
|
}
|
|
|
|
private static ImportedAccount parseAccount(XmlPullParser xpp, List<String> accountUuids,
|
|
boolean overview)
|
|
throws XmlPullParserException, IOException {
|
|
|
|
String uuid = xpp.getAttributeValue(null, SettingsExporter.UUID_ATTRIBUTE);
|
|
|
|
try {
|
|
UUID.fromString(uuid);
|
|
} catch (Exception e) {
|
|
skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT);
|
|
Log.w(K9.LOG_TAG, "Skipping account with invalid UUID " + uuid);
|
|
return null;
|
|
}
|
|
|
|
ImportedAccount account = new ImportedAccount();
|
|
account.uuid = uuid;
|
|
|
|
if (overview || accountUuids.contains(uuid)) {
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.ACCOUNT_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.NAME_ELEMENT.equals(element)) {
|
|
account.name = getText(xpp);
|
|
} else if (SettingsExporter.INCOMING_SERVER_ELEMENT.equals(element)) {
|
|
if (overview) {
|
|
skipToEndTag(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT);
|
|
} else {
|
|
account.incoming = parseServerSettings(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT);
|
|
}
|
|
} else if (SettingsExporter.OUTGOING_SERVER_ELEMENT.equals(element)) {
|
|
if (overview) {
|
|
skipToEndTag(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT);
|
|
} else {
|
|
account.outgoing = parseServerSettings(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT);
|
|
}
|
|
} else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) {
|
|
if (overview) {
|
|
skipToEndTag(xpp, SettingsExporter.SETTINGS_ELEMENT);
|
|
} else {
|
|
account.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT);
|
|
}
|
|
} else if (SettingsExporter.IDENTITIES_ELEMENT.equals(element)) {
|
|
if (overview) {
|
|
skipToEndTag(xpp, SettingsExporter.IDENTITIES_ELEMENT);
|
|
} else {
|
|
account.identities = parseIdentities(xpp);
|
|
}
|
|
} else if (SettingsExporter.FOLDERS_ELEMENT.equals(element)) {
|
|
if (overview) {
|
|
skipToEndTag(xpp, SettingsExporter.FOLDERS_ELEMENT);
|
|
} else {
|
|
account.folders = parseFolders(xpp);
|
|
}
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
} else {
|
|
skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT);
|
|
Log.i(K9.LOG_TAG, "Skipping account with UUID " + uuid);
|
|
}
|
|
|
|
// If we couldn't find an account name use the UUID
|
|
if (account.name == null) {
|
|
account.name = uuid;
|
|
}
|
|
|
|
return account;
|
|
}
|
|
|
|
private static ImportedServer parseServerSettings(XmlPullParser xpp, String endTag)
|
|
throws XmlPullParserException, IOException {
|
|
ImportedServer server = new ImportedServer();
|
|
|
|
server.type = xpp.getAttributeValue(null, SettingsExporter.TYPE_ATTRIBUTE);
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.HOST_ELEMENT.equals(element)) {
|
|
server.host = getText(xpp);
|
|
} else if (SettingsExporter.PORT_ELEMENT.equals(element)) {
|
|
server.port = getText(xpp);
|
|
} else if (SettingsExporter.CONNECTION_SECURITY_ELEMENT.equals(element)) {
|
|
server.connectionSecurity = getText(xpp);
|
|
} else if (SettingsExporter.AUTHENTICATION_TYPE_ELEMENT.equals(element)) {
|
|
server.authenticationType = getText(xpp);
|
|
} else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) {
|
|
server.username = getText(xpp);
|
|
} else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) {
|
|
server.password = getText(xpp);
|
|
} else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) {
|
|
server.extras = parseSettings(xpp, SettingsExporter.EXTRA_ELEMENT);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return server;
|
|
}
|
|
|
|
private static List<ImportedIdentity> parseIdentities(XmlPullParser xpp)
|
|
throws XmlPullParserException, IOException {
|
|
List<ImportedIdentity> identities = null;
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.IDENTITIES_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.IDENTITY_ELEMENT.equals(element)) {
|
|
if (identities == null) {
|
|
identities = new ArrayList<ImportedIdentity>();
|
|
}
|
|
|
|
ImportedIdentity identity = parseIdentity(xpp);
|
|
identities.add(identity);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return identities;
|
|
}
|
|
|
|
private static ImportedIdentity parseIdentity(XmlPullParser xpp)
|
|
throws XmlPullParserException, IOException {
|
|
ImportedIdentity identity = new ImportedIdentity();
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.IDENTITY_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.NAME_ELEMENT.equals(element)) {
|
|
identity.name = getText(xpp);
|
|
} else if (SettingsExporter.EMAIL_ELEMENT.equals(element)) {
|
|
identity.email = getText(xpp);
|
|
} else if (SettingsExporter.DESCRIPTION_ELEMENT.equals(element)) {
|
|
identity.description = getText(xpp);
|
|
} else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) {
|
|
identity.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return identity;
|
|
}
|
|
|
|
private static List<ImportedFolder> parseFolders(XmlPullParser xpp)
|
|
throws XmlPullParserException, IOException {
|
|
List<ImportedFolder> folders = null;
|
|
|
|
int eventType = xpp.next();
|
|
while (!(eventType == XmlPullParser.END_TAG &&
|
|
SettingsExporter.FOLDERS_ELEMENT.equals(xpp.getName()))) {
|
|
|
|
if(eventType == XmlPullParser.START_TAG) {
|
|
String element = xpp.getName();
|
|
if (SettingsExporter.FOLDER_ELEMENT.equals(element)) {
|
|
if (folders == null) {
|
|
folders = new ArrayList<ImportedFolder>();
|
|
}
|
|
|
|
ImportedFolder folder = parseFolder(xpp);
|
|
folders.add(folder);
|
|
} else {
|
|
Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
|
|
}
|
|
}
|
|
eventType = xpp.next();
|
|
}
|
|
|
|
return folders;
|
|
}
|
|
|
|
private static ImportedFolder parseFolder(XmlPullParser xpp)
|
|
throws XmlPullParserException, IOException {
|
|
ImportedFolder folder = new ImportedFolder();
|
|
|
|
String name = xpp.getAttributeValue(null, SettingsExporter.NAME_ATTRIBUTE);
|
|
folder.name = name;
|
|
|
|
folder.settings = parseSettings(xpp, SettingsExporter.FOLDER_ELEMENT);
|
|
|
|
return folder;
|
|
}
|
|
|
|
private static class ImportedServerSettings extends ServerSettings {
|
|
private final ImportedServer mImportedServer;
|
|
|
|
public ImportedServerSettings(ImportedServer server) {
|
|
super(server.type, server.host, convertPort(server.port),
|
|
convertConnectionSecurity(server.connectionSecurity),
|
|
server.authenticationType, server.username, server.password);
|
|
mImportedServer = server;
|
|
}
|
|
|
|
@Override
|
|
public Map<String, String> getExtra() {
|
|
return (mImportedServer.extras != null) ?
|
|
Collections.unmodifiableMap(mImportedServer.extras.settings) : null;
|
|
}
|
|
|
|
private static int convertPort(String port) {
|
|
try {
|
|
return Integer.parseInt(port);
|
|
} catch (NumberFormatException e) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) {
|
|
try {
|
|
return ConnectionSecurity.valueOf(connectionSecurity);
|
|
} catch (Exception e) {
|
|
return ConnectionSecurity.NONE;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class Imported {
|
|
public int contentVersion;
|
|
public ImportedSettings globalSettings;
|
|
public Map<String, ImportedAccount> accounts;
|
|
}
|
|
|
|
private static class ImportedSettings {
|
|
public Map<String, String> settings = new HashMap<String, String>();
|
|
}
|
|
|
|
private static class ImportedAccount {
|
|
public String uuid;
|
|
public String name;
|
|
public ImportedServer incoming;
|
|
public ImportedServer outgoing;
|
|
public ImportedSettings settings;
|
|
public List<ImportedIdentity> identities;
|
|
public List<ImportedFolder> folders;
|
|
}
|
|
|
|
private static class ImportedServer {
|
|
public String type;
|
|
public String host;
|
|
public String port;
|
|
public String connectionSecurity;
|
|
public String authenticationType;
|
|
public String username;
|
|
public String password;
|
|
public ImportedSettings extras;
|
|
}
|
|
|
|
private static class ImportedIdentity {
|
|
public String name;
|
|
public String email;
|
|
public String description;
|
|
public ImportedSettings settings;
|
|
}
|
|
|
|
private static class ImportedFolder {
|
|
public String name;
|
|
public ImportedSettings settings;
|
|
}
|
|
}
|