diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index e40dadb0a..0e11b11a6 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -1,6 +1,8 @@ package com.fsck.k9.activity; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -22,6 +24,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.util.Log; +import android.util.SparseBooleanArray; import android.util.TypedValue; import android.view.ContextMenu; import android.view.Menu; @@ -34,14 +37,17 @@ import android.view.View.OnClickListener; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.ArrayAdapter; +import android.widget.CheckedTextView; import android.widget.ImageButton; import android.widget.LinearLayout; +import android.widget.ListAdapter; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemSelectedListener; import com.fsck.k9.Account; import com.fsck.k9.AccountStats; @@ -64,6 +70,9 @@ import com.fsck.k9.mail.store.StorageManager; import com.fsck.k9.view.ColorChip; import com.fsck.k9.preferences.StorageExporter; import com.fsck.k9.preferences.StorageImportExportException; +import com.fsck.k9.preferences.StorageImporter; +import com.fsck.k9.preferences.StorageImporter.AccountDescription; +import com.fsck.k9.preferences.StorageImporter.ImportContents; public class Accounts extends K9ListActivity implements OnItemClickListener, OnClickListener { @@ -854,53 +863,13 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } private void onImport(Uri uri) { - Toast.makeText(this, "Import is disabled for now", Toast.LENGTH_SHORT).show(); - /* - Log.i(K9.LOG_TAG, "onImport importing from URI " + uri.getPath()); + //Toast.makeText(this, "Import is disabled for now", Toast.LENGTH_SHORT).show(); - final String fileName = uri.getPath(); - AsyncUIProcessor.getInstance(Accounts.this.getApplication()).importSettings(this, uri, new ImportListener() { - @Override - public void success(int numAccounts) { - mHandler.progress(false); - String accountQtyText = getResources().getQuantityString(R.plurals.settings_import_success, numAccounts, numAccounts); - String messageText = getString(R.string.settings_import_success, accountQtyText, fileName); - showDialog(Accounts.this, R.string.settings_import_success_header, messageText); - runOnUiThread(new Runnable() { - @Override - public void run() { - refresh(); - } - }); - } + Log.i(K9.LOG_TAG, "onImport importing from URI " + uri.toString()); - @Override - public void failure(String message, Exception e) { - mHandler.progress(false); - showDialog(Accounts.this, R.string.settings_import_failed_header, Accounts.this.getString(R.string.settings_import_failure, fileName, e.getLocalizedMessage())); - } - - @Override - public void canceled() { - mHandler.progress(false); - } - - @Override - public void started() { - runOnUiThread(new Runnable() { - @Override - public void run() { - mHandler.progress(true); - String toastText = Accounts.this.getString(R.string.settings_importing); - Toast toast = Toast.makeText(Accounts.this, toastText, Toast.LENGTH_SHORT); - toast.show(); - } - }); - - } - }); - */ + new ListImportContentsAsyncTask(uri, null).execute(); } + private void showDialog(final Context context, final int headerRes, final String message) { this.runOnUiThread(new Runnable() { @Override @@ -1180,7 +1149,6 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC @Override protected void onPostExecute(Boolean success) { - setProgress(false); if (success) { showDialog(Accounts.this, R.string.settings_export_success_header, Accounts.this.getString(R.string.settings_export_success, mFileName)); @@ -1191,4 +1159,166 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } } + + private class ImportAsyncTask extends AsyncTask { + private boolean mIncludeGlobals; + private Set mAccountUuids; + private boolean mOverwrite; + private String mEncryptionKey; + private InputStream mInputStream; + + private ImportAsyncTask(boolean includeGlobals, Set accountUuids, + boolean overwrite, String encryptionKey, InputStream is) { + mIncludeGlobals = includeGlobals; + mAccountUuids = accountUuids; + mOverwrite = overwrite; + mEncryptionKey = encryptionKey; + mInputStream = is; + } + + @Override + protected void onPreExecute() { + //TODO: show progress bar instead of displaying toast + String toastText = Accounts.this.getString(R.string.settings_importing); + Toast toast = Toast.makeText(Accounts.this, toastText, Toast.LENGTH_SHORT); + toast.show(); + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + StorageImporter.importSettings(Accounts.this, mInputStream, mEncryptionKey, + mIncludeGlobals, mAccountUuids, mOverwrite); + } catch (StorageImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + showDialog(Accounts.this, R.string.settings_import_success_header, + //FIXME: use correct number of imported accounts + Accounts.this.getString(R.string.settings_import_success, 3, "unknown")); + refresh(); + } else { + //TODO: make the importer return an error code; translate that error code to a localized string here + showDialog(Accounts.this, R.string.settings_import_failed_header, + Accounts.this.getString(R.string.settings_import_failure, "unknown", "Something went wrong")); + } + } + } + + ImportContents mImportContents; + private class ListImportContentsAsyncTask extends AsyncTask { + private Uri mUri; + private String mEncryptionKey; + private InputStream mInputStream; + + private ListImportContentsAsyncTask(Uri uri, String encryptionKey) { + mUri = uri; + mEncryptionKey = encryptionKey; + } + + @Override + protected void onPreExecute() { + //TODO: show progress bar + } + + @Override + protected Boolean doInBackground(Void... params) { + try { + + InputStream is = getContentResolver().openInputStream(mUri); + mImportContents = StorageImporter.getImportStreamContents( + Accounts.this, is, mEncryptionKey); + + // Open another InputStream in the background. This is used later by ImportAsyncTask + mInputStream = getContentResolver().openInputStream(mUri); + + } catch (StorageImportExportException e) { + Log.w(K9.LOG_TAG, "Exception during export", e); + return false; + } + catch (FileNotFoundException e) { + Log.w(K9.LOG_TAG, "Couldn't read content from URI " + mUri); + return false; + } + return true; + } + + @Override + protected void onPostExecute(Boolean success) { + if (success) { + final ListView importSelectionView = new ListView(Accounts.this); + List contents = new ArrayList(); + if (mImportContents.globalSettings) { + contents.add("Global settings"); + } + for (AccountDescription account : mImportContents.accounts) { + contents.add(account.name); + } + importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + importSelectionView.setAdapter(new ArrayAdapter(Accounts.this, android.R.layout.simple_list_item_checked, contents)); + importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, View view, int pos, long id) { + CheckedTextView ctv = (CheckedTextView)view; + ctv.setChecked(!ctv.isChecked()); + } + + @Override + public void onNothingSelected(AdapterView arg0) {} + }); + + //TODO: listview header: "Please select the settings you wish to import" + //TODO: listview footer: "Select all" / "Select none" buttons? + //TODO: listview footer: "Overwrite existing accounts?" checkbox + + final AlertDialog.Builder builder = new AlertDialog.Builder(Accounts.this); + builder.setTitle("Import selection"); + builder.setView(importSelectionView); + builder.setInverseBackgroundForced(true); + builder.setPositiveButton(R.string.okay_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + ListAdapter adapter = importSelectionView.getAdapter(); + int count = adapter.getCount(); + SparseBooleanArray pos = importSelectionView.getCheckedItemPositions(); + + boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false; + Set accountUuids = new HashSet(); + for (int i = 1; i < count; i++) { + if (pos.get(i)) { + accountUuids.add(mImportContents.accounts.get(i-1).uuid); + } + } + + boolean overwrite = false; //TODO: get value from dialog + + dialog.dismiss(); + new ImportAsyncTask(includeGlobals, accountUuids, overwrite, mEncryptionKey, mInputStream).execute(); + } + }); + builder.setNegativeButton(R.string.cancel_action, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + try { + mInputStream.close(); + } catch (Exception e) { /* Ignore */ } + } + }); + builder.show(); + } else { + //TODO: make the importer return an error code; translate that error code to a localized string here + showDialog(Accounts.this, R.string.settings_import_failed_header, + Accounts.this.getString(R.string.settings_import_failure, "unknown", "Something went wrong")); + } + } + } } diff --git a/src/com/fsck/k9/preferences/StorageImporter.java b/src/com/fsck/k9/preferences/StorageImporter.java index f87183e20..47f15fa08 100644 --- a/src/com/fsck/k9/preferences/StorageImporter.java +++ b/src/com/fsck/k9/preferences/StorageImporter.java @@ -1,190 +1,716 @@ package com.fsck.k9.preferences; -import java.io.BufferedReader; +import java.io.IOException; import java.io.InputStream; -import java.io.StringReader; +import java.io.InputStreamReader; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Stack; +import java.util.Set; import java.util.UUID; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import org.xml.sax.Attributes; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.XMLReader; -import org.xml.sax.helpers.DefaultHandler; +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.DateFormatter; +import com.fsck.k9.helper.Utility; public class StorageImporter { - public static void importPreferences(Context context, InputStream is, String encryptionKey, - boolean globalSettings, String[] importAccountUuids, boolean overwrite) - throws StorageImportExportException { + /** + * Class to list the contents of an import file/stream. + * + * @see StorageImporter#getImportStreamContents(Context,InputStream,String) + */ + 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 accounts; + + private ImportContents(boolean globalSettings, List 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 boolean isImportStreamEncrypted(Context context, InputStream inputStream) { + return false; + } + + /** + * 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 context + * @param inputStream + * @param encryptionKey + * @return + * @throws StorageImportExportException + */ + public static ImportContents getImportStreamContents(Context context, InputStream inputStream, + String encryptionKey) throws StorageImportExportException { try { - SAXParserFactory spf = SAXParserFactory.newInstance(); - SAXParser sp = spf.newSAXParser(); - XMLReader xr = sp.getXMLReader(); - StorageImporterHandler handler = new StorageImporterHandler(); - xr.setContentHandler(handler); + // Parse the import stream but don't save individual settings (overview=true) + Imported imported = parseSettings(inputStream, false, null, false, true); - xr.parse(new InputSource(is)); + // If the stream contains global settings the "globalSettings" member will not be null + boolean globalSettings = (imported.globalSettings != null); - ImportElement dataset = handler.getRootElement(); - String storageFormat = dataset.attributes.get("version"); - Log.i(K9.LOG_TAG, "Got settings file version " + storageFormat); + final List accounts = new ArrayList(); + // 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)); + } + } + + return new ImportContents(globalSettings, accounts); + + } catch (StorageImportExportException e) { + throw e; + } catch (Exception e) { + throw new StorageImportExportException(e); + } + } + + /** + * Reads an import {@link InputStream} and imports the global settings and/or account + * configurations specified by the arguments. + * + * @param context + * @param inputStream + * @param encryptionKey + * @param globalSettings + * @param accountUuids + * @param overwrite + * @throws StorageImportExportException + */ + public static void importSettings(Context context, InputStream inputStream, String encryptionKey, + boolean globalSettings, Set accountUuids, boolean overwrite) + throws StorageImportExportException { + + try + { + Imported imported = parseSettings(inputStream, globalSettings, accountUuids, overwrite, false); Preferences preferences = Preferences.getPreferences(context); SharedPreferences storage = preferences.getPreferences(); SharedPreferences.Editor editor = storage.edit(); - String data = dataset.data.toString(); - List accountNumbers = Account.getExistingAccountNumbers(preferences); - Log.i(K9.LOG_TAG, "Existing accountNumbers = " + accountNumbers); - /** - * We translate UUIDs in the import file into new UUIDs in the local instance for the following reasons: - * 1) Accidentally importing the same file twice cannot damage settings in an existing account. - * (Say, an account that was imported two months ago and has since had significant settings changes.) - * 2) Importing a single file multiple times allows for creating multiple accounts from the same template. - * 3) Exporting an account and importing back into the same instance is a poor-man's account copy (until a real - * copy function is created, if ever) - */ - Map uuidMapping = new HashMap(); - String accountUuids = preferences.getPreferences().getString("accountUuids", null); - - StringReader sr = new StringReader(data); - BufferedReader br = new BufferedReader(sr); - String line = null; - int settingsImported = 0; - int numAccounts = 0; - K9Krypto krypto = new K9Krypto(encryptionKey, K9Krypto.MODE.DECRYPT); - do { - line = br.readLine(); - if (line != null) { - //Log.i(K9.LOG_TAG, "Got line " + line); - String[] comps = line.split(":"); - if (comps.length > 1) { - String keyEnc = comps[0]; - String valueEnc = comps[1]; - String key = krypto.decrypt(keyEnc); - String value = krypto.decrypt(valueEnc); - String[] keyParts = key.split("\\."); - if (keyParts.length > 1) { - String oldUuid = keyParts[0]; - String newUuid = uuidMapping.get(oldUuid); - if (newUuid == null) { - newUuid = UUID.randomUUID().toString(); - uuidMapping.put(oldUuid, newUuid); - - Log.i(K9.LOG_TAG, "Mapping oldUuid " + oldUuid + " to newUuid " + newUuid); - } - keyParts[0] = newUuid; - if ("accountNumber".equals(keyParts[1])) { - int accountNumber = Account.findNewAccountNumber(accountNumbers); - accountNumbers.add(accountNumber); - value = Integer.toString(accountNumber); - accountUuids += (accountUuids.length() != 0 ? "," : "") + newUuid; - numAccounts++; - } - StringBuilder builder = new StringBuilder(); - for (String part : keyParts) { - if (builder.length() > 0) { - builder.append("."); - } - builder.append(part); - } - key = builder.toString(); - } - //Log.i(K9.LOG_TAG, "Setting " + key + " = " + value); - settingsImported++; - editor.putString(key, value); - } + if (globalSettings) { + if (imported.globalSettings != null) { + importGlobalSettings(editor, imported.globalSettings); + } else { + Log.w(K9.LOG_TAG, "Was asked to import global settings but none found."); } + } - } while (line != null); + if (accountUuids != null && accountUuids.size() > 0) { + if (imported.accounts != null) { + List newUuids = new ArrayList(); + for (String accountUuid : accountUuids) { + if (imported.accounts.containsKey(accountUuid)) { + String newUuid = importAccount(context, editor, imported.accounts.get(accountUuid), overwrite); + if (newUuid != null) { + newUuids.add(newUuid); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import account with UUID " + + accountUuid + ". But this account wasn't found."); + } + } + if (newUuids.size() > 0) { + String oldAccountUuids = storage.getString("accountUuids", ""); + String appendUuids = Utility.combine(newUuids.toArray(new String[0]), ','); + String prefix = ""; + if (oldAccountUuids.length() > 0) { + prefix = oldAccountUuids + ","; + } + editor.putString("accountUuids", prefix + appendUuids); + } + } else { + Log.w(K9.LOG_TAG, "Was asked to import at least one account but none found."); + } + } - editor.putString("accountUuids", accountUuids); - Log.i(K9.LOG_TAG, "Imported " + settingsImported + " settings and " + numAccounts + " accounts"); + if (!editor.commit()) { + throw new StorageImportExportException("Couldn't save imported settings"); + } - editor.commit(); - Preferences.getPreferences(context).refreshAccounts(); + preferences.refreshAccounts(); DateFormatter.clearChosenFormat(); - K9.loadPrefs(Preferences.getPreferences(context)); + K9.loadPrefs(preferences); K9.setServicesEnabled(context); + } catch (StorageImportExportException e) { + throw e; } catch (Exception e) { - throw new StorageImportExportException(); + throw new StorageImportExportException(e); } } + private static void importGlobalSettings(SharedPreferences.Editor editor, + ImportedSettings settings) { + //TODO: input validation - public static class ImportElement { - String name; - Map attributes = new HashMap(); - Map subElements = new HashMap(); - StringBuilder data = new StringBuilder(); + for (Map.Entry setting : settings.settings.entrySet()) { + String key = setting.getKey(); + String value = setting.getValue(); + //FIXME: drop this key during input validation. then remove this check + if ("accountUuids".equals(key)) { + continue; + } + editor.putString(key, value); + } } - private static class StorageImporterHandler extends DefaultHandler { - private ImportElement rootElement = new ImportElement(); - private Stack mOpenTags = new Stack(); + private static String importAccount(Context context, SharedPreferences.Editor editor, + ImportedAccount account, boolean overwrite) { - public ImportElement getRootElement() { - return this.rootElement; + //TODO: input validation + //TODO: remove latestOldMessageSeenTime? + + Preferences prefs = Preferences.getPreferences(context); + Account[] accounts = prefs.getAccounts(); + + String uuid = account.uuid; + Account existingAccount = prefs.getAccount(uuid); + 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(); } - @Override - public void startDocument() throws SAXException { - } - - @Override - public void endDocument() throws SAXException { - /* Do nothing */ - } - - @Override - public void startElement(String namespaceURI, String localName, - String qName, Attributes attributes) throws SAXException { - Log.i(K9.LOG_TAG, "Starting element " + localName); - ImportElement element = new ImportElement(); - element.name = localName; - mOpenTags.push(element); - for (int i = 0; i < attributes.getLength(); i++) { - String key = attributes.getLocalName(i); - String value = attributes.getValue(i); - Log.i(K9.LOG_TAG, "Got attribute " + key + " = " + value); - element.attributes.put(key, value); + 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; + } } } - @Override - public void endElement(String namespaceURI, String localName, String qName) { - Log.i(K9.LOG_TAG, "Ending element " + localName); - ImportElement element = mOpenTags.pop(); - ImportElement superElement = mOpenTags.empty() ? null : mOpenTags.peek(); - if (superElement != null) { - superElement.subElements.put(element.name, element); - } else { - rootElement = element; + String accountKeyPrefix = uuid + "."; + editor.putString(accountKeyPrefix + Account.ACCOUNT_DESCRIPTION_KEY, accountName); + + // Write account settings + for (Map.Entry setting : account.settings.settings.entrySet()) { + //FIXME: drop this key during input validation. then remove this check + if ("accountNumber".equals(setting.getKey())) { + continue; + } + + String key = accountKeyPrefix + setting.getKey(); + String value = setting.getValue(); + editor.putString(key, value); + } + + // If it's a new account generate and write a new "accountNumber" + if (existingAccount == null || !uuid.equals(account.uuid)) { + int newAccountNumber = Account.generateAccountNumber(prefs); + editor.putString(accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber)); + } + + if (account.identities != null) { + importIdentities(editor, uuid, account, overwrite, existingAccount); + } + + // Write folder settings + if (account.folders != null) { + for (ImportedFolder folder : account.folders) { + String folderKeyPrefix = uuid + "." + folder.name + "."; + for (Map.Entry setting : folder.settings.settings.entrySet()) { + String key = folderKeyPrefix + setting.getKey(); + String value = setting.getValue(); + editor.putString(key, value); + } } } - @Override - public void characters(char ch[], int start, int length) { - String value = new String(ch, start, length); - mOpenTags.peek().data.append(value); + //TODO: sync folder settings with localstore? + + return (overwrite && existingAccount != null) ? null : uuid; + } + + private static void importIdentities(SharedPreferences.Editor editor, String uuid, + ImportedAccount account, boolean overwrite, Account existingAccount) { + + String accountKeyPrefix = uuid + "."; + + // Gather information about existing identities for this account (if any) + int nextIdentityIndex = 0; + final List existingIdentities; + if (overwrite && existingAccount != null) { + existingIdentities = existingAccount.getIdentities(); + nextIdentityIndex = existingIdentities.size(); + } else { + existingIdentities = new ArrayList(); } + + // Write identities + for (ImportedIdentity identity : account.identities) { + int writeIdentityIndex = nextIdentityIndex; + if (existingIdentities.size() > 0) { + int identityIndex = findIdentity(identity, existingIdentities); + if (overwrite && identityIndex != -1) { + writeIdentityIndex = identityIndex; + } + } + if (writeIdentityIndex == nextIdentityIndex) { + nextIdentityIndex++; + } + + String identityDescription = 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; + } + } + } + + editor.putString(accountKeyPrefix + Account.IDENTITY_NAME_KEY + "." + + writeIdentityIndex, identity.name); + editor.putString(accountKeyPrefix + Account.IDENTITY_EMAIL_KEY + "." + + writeIdentityIndex, identity.email); + editor.putString(accountKeyPrefix + Account.IDENTITY_DESCRIPTION_KEY + "." + + writeIdentityIndex, identityDescription); + + // Write identity settings + for (Map.Entry setting : identity.settings.settings.entrySet()) { + String key = setting.getKey(); + String value = setting.getValue(); + editor.putString(accountKeyPrefix + key + "." + writeIdentityIndex, 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 identities) { + for (Identity identitiy : identities) { + if (identitiy.getDescription().equals(description)) { + return true; + } + } + return false; + } + + private static int findIdentity(ImportedIdentity identity, + List 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; + } + + private static Imported parseSettings(InputStream inputStream, boolean globalSettings, + Set accountUuids, boolean overwrite, boolean overview) + throws StorageImportExportException { + + 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 (StorageExporter.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 StorageImportExportException("Invalid import data"); + } + + return imported; + } catch (Exception e) { + throw new StorageImportExportException(e); + } finally { + try { + inputStream.close(); + } catch (Exception e) { /* Ignore */ } + } + } + + 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 null; + } + return xpp.getText(); + } + + private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings, + Set accountUuids, boolean overview) + throws XmlPullParserException, IOException { + + Imported result = new Imported(); + + //TODO: check version attribute + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + StorageExporter.ROOT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.GLOBAL_ELEMENT.equals(element)) { + if (overview || globalSettings) { + if (result.globalSettings == null) { + if (overview) { + result.globalSettings = new ImportedSettings(); + skipToEndTag(xpp, StorageExporter.GLOBAL_ELEMENT); + } else { + result.globalSettings = parseSettings(xpp, StorageExporter.GLOBAL_ELEMENT); + } + } else { + skipToEndTag(xpp, StorageExporter.GLOBAL_ELEMENT); + Log.w(K9.LOG_TAG, "More than one global settings element. Only using the first one!"); + } + } else { + skipToEndTag(xpp, StorageExporter.GLOBAL_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping global settings"); + } + } else if (StorageExporter.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 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 (StorageExporter.VALUE_ELEMENT.equals(element)) { + String key = xpp.getAttributeValue(null, StorageExporter.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 parseAccounts(XmlPullParser xpp, + Set accountUuids, boolean overview) + throws XmlPullParserException, IOException { + + Map accounts = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + StorageExporter.ACCOUNTS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.ACCOUNT_ELEMENT.equals(element)) { + if (accounts == null) { + accounts = new HashMap(); + } + + ImportedAccount account = parseAccount(xpp, accountUuids, overview); + + 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, Set accountUuids, + boolean overview) + throws XmlPullParserException, IOException { + + ImportedAccount account = new ImportedAccount(); + + String uuid = xpp.getAttributeValue(null, StorageExporter.UUID_ATTRIBUTE); + account.uuid = uuid; + + if (overview || accountUuids.contains(uuid)) { + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + StorageExporter.ACCOUNT_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.NAME_ELEMENT.equals(element)) { + account.name = getText(xpp); + } else if (StorageExporter.SETTINGS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, StorageExporter.SETTINGS_ELEMENT); + } else { + account.settings = parseSettings(xpp, StorageExporter.SETTINGS_ELEMENT); + } + } else if (StorageExporter.IDENTITIES_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, StorageExporter.IDENTITIES_ELEMENT); + } else { + account.identities = parseIdentities(xpp); + } + } else if (StorageExporter.FOLDERS_ELEMENT.equals(element)) { + if (overview) { + skipToEndTag(xpp, StorageExporter.FOLDERS_ELEMENT); + } else { + account.folders = parseFolders(xpp); + } + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + } else { + skipToEndTag(xpp, StorageExporter.ACCOUNT_ELEMENT); + Log.i(K9.LOG_TAG, "Skipping account with UUID " + uuid); + } + + return account; + } + + private static List parseIdentities(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List identities = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + StorageExporter.IDENTITIES_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.IDENTITY_ELEMENT.equals(element)) { + if (identities == null) { + identities = new ArrayList(); + } + + 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 && + StorageExporter.IDENTITY_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.NAME_ELEMENT.equals(element)) { + identity.name = getText(xpp); + } else if (StorageExporter.EMAIL_ELEMENT.equals(element)) { + identity.email = getText(xpp); + } else if (StorageExporter.DESCRIPTION_ELEMENT.equals(element)) { + identity.description = getText(xpp); + } else if (StorageExporter.SETTINGS_ELEMENT.equals(element)) { + identity.settings = parseSettings(xpp, StorageExporter.SETTINGS_ELEMENT); + } else { + Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName()); + } + } + eventType = xpp.next(); + } + + return identity; + } + + private static List parseFolders(XmlPullParser xpp) + throws XmlPullParserException, IOException { + List folders = null; + + int eventType = xpp.next(); + while (!(eventType == XmlPullParser.END_TAG && + StorageExporter.FOLDERS_ELEMENT.equals(xpp.getName()))) { + + if(eventType == XmlPullParser.START_TAG) { + String element = xpp.getName(); + if (StorageExporter.FOLDER_ELEMENT.equals(element)) { + if (folders == null) { + folders = new ArrayList(); + } + + 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, StorageExporter.NAME_ATTRIBUTE); + folder.name = name; + + folder.settings = parseSettings(xpp, StorageExporter.FOLDER_ELEMENT); + + return folder; + } + + private static class Imported { + public ImportedSettings globalSettings; + public Map accounts; + } + + private static class ImportedSettings { + public Map settings = new HashMap(); + } + + private static class ImportedAccount { + public String uuid; + public String name; + public ImportedSettings settings; + public List identities; + public List folders; + } + + 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; } }