diff --git a/src/com/fsck/k9/activity/AsyncUIProcessor.java b/src/com/fsck/k9/activity/AsyncUIProcessor.java
new file mode 100644
index 000000000..579c773ea
--- /dev/null
+++ b/src/com/fsck/k9/activity/AsyncUIProcessor.java
@@ -0,0 +1,125 @@
+package com.fsck.k9.activity;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import android.app.Application;
+import android.os.Environment;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.preferences.StorageExporter;
+import com.fsck.k9.preferences.StorageImporter;
+
+/**
+ * The class should be used to run long-running processes invoked from the UI that
+ * do not affect the Stores. There are probably pieces of MessagingController
+ * that can be moved here.
+ *
+ */
+public class AsyncUIProcessor
+{
+
+ private final ExecutorService threadPool = Executors.newCachedThreadPool();
+ private Application mApplication;
+ private static AsyncUIProcessor inst = null;
+ private AsyncUIProcessor(Application application)
+ {
+ mApplication = application;
+ }
+ public synchronized static AsyncUIProcessor getInstance(Application application)
+ {
+ if (inst == null)
+ {
+ inst = new AsyncUIProcessor(application);
+ }
+ return inst;
+ }
+ public void exportSettings(final String uuid, final String encryptionKey, final ExportListener listener)
+ {
+ threadPool.execute(new Runnable()
+ {
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ // Do not store with application files. Settings exports should *not* be
+ // deleted when the application is uninstalled
+ File dir = new File(Environment.getExternalStorageDirectory() + File.separator
+ + mApplication.getPackageName());
+ dir.mkdirs();
+ File file = Utility.createUniqueFile(dir, "settings.k9s");
+ String fileName = file.getAbsolutePath();
+ StorageExporter.exportPreferences(mApplication, uuid, fileName, encryptionKey);
+ if (listener != null)
+ {
+ listener.exportSuccess(fileName);
+ }
+ }
+ catch (Exception e)
+ {
+ listener.failure(e.getLocalizedMessage(), e);
+ }
+ }
+ }
+ );
+
+ }
+ public void importSettings(final String fileName, final String encryptionKey, final ImportListener listener)
+ {
+ threadPool.execute(new Runnable()
+ {
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ int numAccounts = StorageImporter.importPreferences(mApplication, fileName, encryptionKey);
+ K9.setServicesEnabled(mApplication);
+ if (listener != null)
+ {
+ listener.importSuccess(numAccounts);
+ }
+ }
+ catch (Exception e)
+ {
+ listener.failure(e.getLocalizedMessage(), e);
+ }
+ }
+ }
+ );
+
+ }
+ public void importSettings(final InputStream inputStream, final String encryptionKey, final ImportListener listener)
+ {
+ threadPool.execute(new Runnable()
+ {
+
+ @Override
+ public void run()
+ {
+ try
+ {
+ int numAccounts = StorageImporter.importPreferences(mApplication, inputStream, encryptionKey);
+ K9.setServicesEnabled(mApplication);
+ if (listener != null)
+ {
+ listener.importSuccess(numAccounts);
+ }
+ }
+ catch (Exception e)
+ {
+ listener.failure(e.getLocalizedMessage(), e);
+ }
+ }
+ }
+ );
+
+ }
+
+}
diff --git a/src/com/fsck/k9/activity/ExportHelper.java b/src/com/fsck/k9/activity/ExportHelper.java
new file mode 100644
index 000000000..ebb7752bc
--- /dev/null
+++ b/src/com/fsck/k9/activity/ExportHelper.java
@@ -0,0 +1,67 @@
+package com.fsck.k9.activity;
+
+import android.app.Activity;
+import android.widget.Toast;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.R;
+
+public class ExportHelper
+{
+ public static void exportSettings(final Activity activity, final Progressable progressable, final Account account)
+ {
+ PasswordEntryDialog dialog = new PasswordEntryDialog(activity, activity.getString(R.string.settings_encryption_password_prompt),
+ new PasswordEntryDialog.PasswordEntryListener()
+ {
+ public void passwordChosen(String chosenPassword)
+ {
+ String toastText = activity.getString(R.string.settings_exporting );
+ Toast toast = Toast.makeText(activity, toastText, Toast.LENGTH_SHORT);
+ toast.show();
+ progressable.setProgress(true);
+ String uuid = null;
+ if (account != null)
+ {
+ uuid = account.getUuid();
+ }
+ AsyncUIProcessor.getInstance(activity.getApplication()).exportSettings(uuid, chosenPassword,
+ new ExportListener()
+ {
+ public void failure(final String message, Exception e)
+ {
+ activity.runOnUiThread(new Runnable()
+ {
+ public void run()
+ {
+ progressable.setProgress(false);
+ String toastText = activity.getString(R.string.settings_export_failure, message);
+ Toast toast = Toast.makeText(activity.getApplication(), toastText, Toast.LENGTH_LONG);
+ toast.show();
+ }
+ });
+ }
+
+ public void exportSuccess(final String fileName)
+ {
+ activity.runOnUiThread(new Runnable()
+ {
+ public void run()
+ {
+ progressable.setProgress(false);
+ String toastText = activity.getString(R.string.settings_export_success, fileName );
+ Toast toast = Toast.makeText(activity.getApplication(), toastText, Toast.LENGTH_LONG);
+ toast.show();
+ }
+ });
+ }
+ });
+ }
+
+ public void cancel()
+ {
+ }
+ });
+ dialog.show();
+ }
+
+}
diff --git a/src/com/fsck/k9/activity/ExportListener.java b/src/com/fsck/k9/activity/ExportListener.java
new file mode 100644
index 000000000..d1e6bbc6c
--- /dev/null
+++ b/src/com/fsck/k9/activity/ExportListener.java
@@ -0,0 +1,9 @@
+package com.fsck.k9.activity;
+
+public interface ExportListener
+{
+ public void exportSuccess(String fileName);
+
+ public void failure(String message, Exception e);
+
+}
diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index 6ef59138d..9db7ccc0c 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -174,7 +174,11 @@ public class FolderList extends K9ListActivity
});
}
}
-
+
+ public void setProgress(boolean progress)
+ {
+ mHandler.progress(progress);
+ }
/**
* This class is responsible for reloading the list of local messages for a
* given folder, notifying the adapter that the message have been loaded and
@@ -628,6 +632,12 @@ public class FolderList extends K9ListActivity
case R.id.compact:
onCompact(mAccount);
+ case R.id.export:
+ onExport(mAccount);
+ return true;
+
+ case R.id.export_all:
+ onExport(null);
return true;
case R.id.display_1st_class:
diff --git a/src/com/fsck/k9/activity/ImportListener.java b/src/com/fsck/k9/activity/ImportListener.java
new file mode 100644
index 000000000..d809baa90
--- /dev/null
+++ b/src/com/fsck/k9/activity/ImportListener.java
@@ -0,0 +1,9 @@
+package com.fsck.k9.activity;
+
+public interface ImportListener
+{
+ public void importSuccess(int numAccounts);
+
+ public void failure(String message, Exception e);
+
+}
diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java
index f59b63014..6423d31f3 100644
--- a/src/com/fsck/k9/activity/K9Activity.java
+++ b/src/com/fsck/k9/activity/K9Activity.java
@@ -15,11 +15,13 @@ import android.view.animation.AccelerateInterpolator;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.ScrollView;
+
+import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.helper.DateFormatter;
-public class K9Activity extends Activity
+public class K9Activity extends Activity implements Progressable
{
private GestureDetector gestureDetector;
@@ -195,6 +197,13 @@ public class K9Activity extends Activity
return false;
}
}
-
-
+ public void setProgress(boolean progress)
+ {
+ }
+
+ public void onExport(final Account account)
+ {
+ ExportHelper.exportSettings(this, this, account);
+ }
+
}
diff --git a/src/com/fsck/k9/activity/K9ListActivity.java b/src/com/fsck/k9/activity/K9ListActivity.java
index 4a9d60aea..f9c980df2 100644
--- a/src/com/fsck/k9/activity/K9ListActivity.java
+++ b/src/com/fsck/k9/activity/K9ListActivity.java
@@ -5,11 +5,16 @@ import android.util.Log;
import android.view.KeyEvent;
import android.widget.AdapterView;
import android.widget.ListView;
+import android.widget.Toast;
import android.os.Bundle;
+
+import com.fsck.k9.Account;
import com.fsck.k9.K9;
+import com.fsck.k9.R;
+import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.helper.DateFormatter;
-public class K9ListActivity extends ListActivity
+public class K9ListActivity extends ListActivity implements Progressable
{
@Override
public void onCreate(Bundle icicle)
@@ -106,4 +111,14 @@ public class K9ListActivity extends ListActivity
}
return super.onKeyUp(keyCode,event);
}
+
+ public void setProgress(boolean progress)
+ {
+ }
+
+ public void onExport(final Account account)
+ {
+ ExportHelper.exportSettings(this, this, account);
+ }
+
}
diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 57f32a4ad..de3968480 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -80,7 +80,7 @@ import com.fsck.k9.mail.store.LocalStore.LocalFolder;
*/
public class MessageList
extends K9Activity
- implements OnClickListener, AdapterView.OnItemClickListener, AnimationListener
+ implements OnClickListener, AdapterView.OnItemClickListener, AnimationListener, Progressable
{
/**
@@ -596,6 +596,10 @@ public class MessageList
});
}
}
+ public void setProgress(boolean progress)
+ {
+ mHandler.progress(progress);
+ }
public static void actionHandleFolder(Context context, Account account, String folder)
{
@@ -1679,6 +1683,14 @@ public class MessageList
onEditPrefs();
return true;
}
+ case R.id.export:
+ onExport(mAccount);
+ return true;
+
+ case R.id.export_all:
+ onExport(null);
+ return true;
+
}
if (mQueryString != null)
diff --git a/src/com/fsck/k9/activity/PasswordEntryDialog.java b/src/com/fsck/k9/activity/PasswordEntryDialog.java
new file mode 100644
index 000000000..ae58dabdb
--- /dev/null
+++ b/src/com/fsck/k9/activity/PasswordEntryDialog.java
@@ -0,0 +1,90 @@
+package com.fsck.k9.activity;
+
+import com.fsck.k9.R;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.text.Editable;
+import android.text.TextWatcher;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+
+public class PasswordEntryDialog
+{
+ public interface PasswordEntryListener
+ {
+ void passwordChosen(String chosenPassword);
+ void cancel();
+ }
+ PasswordEntryListener listener;
+ private EditText passwordView;
+ AlertDialog dialog;
+ public PasswordEntryDialog(Context context, String headerText, PasswordEntryListener listener )
+ {
+ this.listener = listener;
+ View view = LayoutInflater.from(context).inflate(R.layout.password_entry_dialog, null);
+ Builder builder = new AlertDialog.Builder(context);
+ passwordView = (EditText)view.findViewById(R.id.password_text_box);
+
+ builder.setView(view);
+ builder.setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ if (PasswordEntryDialog.this.listener != null)
+ {
+ String chosenPassword = passwordView.getText().toString();
+ PasswordEntryDialog.this.listener.passwordChosen(chosenPassword);
+ }
+ }
+ });
+ builder.setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener()
+ {
+ @Override
+ public void onClick(DialogInterface dialog, int which)
+ {
+ if (PasswordEntryDialog.this.listener != null)
+ {
+ PasswordEntryDialog.this.listener.cancel();
+ }
+ }
+ });
+ dialog = builder.create();
+ passwordView.addTextChangedListener(new TextWatcher()
+ {
+
+ @Override
+ public void afterTextChanged(Editable arg0) { }
+
+ @Override
+ public void beforeTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) { }
+
+ @Override
+ public void onTextChanged(CharSequence arg0, int arg1, int arg2,
+ int arg3)
+ {
+
+ Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ String chosenPassword = passwordView.getText().toString();
+ okButton.setEnabled(chosenPassword.length() > 0);
+
+ }
+ });
+
+ dialog.setMessage(headerText);
+
+
+ }
+ public void show()
+ {
+ dialog.show();
+ Button okButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE);
+ okButton.setEnabled(false);
+ }
+
+}
diff --git a/src/com/fsck/k9/activity/Progressable.java b/src/com/fsck/k9/activity/Progressable.java
new file mode 100644
index 000000000..f38bffce8
--- /dev/null
+++ b/src/com/fsck/k9/activity/Progressable.java
@@ -0,0 +1,6 @@
+package com.fsck.k9.activity;
+
+public interface Progressable
+{
+ public void setProgress(boolean progress);
+}
diff --git a/src/com/fsck/k9/preferences/IStorageImporter.java b/src/com/fsck/k9/preferences/IStorageImporter.java
new file mode 100644
index 000000000..4d8068923
--- /dev/null
+++ b/src/com/fsck/k9/preferences/IStorageImporter.java
@@ -0,0 +1,10 @@
+package com.fsck.k9.preferences;
+
+import com.fsck.k9.Preferences;
+
+import android.content.SharedPreferences;
+
+public interface IStorageImporter
+{
+ public abstract int importPreferences(Preferences preferences, SharedPreferences.Editor context, String data, String encryptionKey) throws StorageImportExportException;
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/preferences/SimpleCrypto.java b/src/com/fsck/k9/preferences/SimpleCrypto.java
new file mode 100644
index 000000000..4ccb4a8b1
--- /dev/null
+++ b/src/com/fsck/k9/preferences/SimpleCrypto.java
@@ -0,0 +1,90 @@
+package com.fsck.k9.preferences;
+
+import java.security.SecureRandom;
+
+import javax.crypto.Cipher;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.codec.binary.Base64;
+
+
+
+/**
+ * package net.sf.andhsli.hotspotlogin;
+ * Usage:
+ *
+ * String crypto = SimpleCrypto.encrypt(masterpassword, cleartext)
+ * ...
+ * String cleartext = SimpleCrypto.decrypt(masterpassword, crypto)
+ *
+ * @author ferenc.hechler
+ */
+public class SimpleCrypto {
+
+ public static String encrypt(String seed, String cleartext, Base64 base64) throws Exception {
+ byte[] rawKey = getRawKey(seed.getBytes());
+ byte[] result = encrypt(rawKey, cleartext.getBytes());
+ return new String(base64.encode(result));
+ }
+
+ public static String decrypt(String seed, String encrypted, Base64 base64) throws Exception {
+ byte[] rawKey = getRawKey(seed.getBytes());
+ byte[] enc = base64.decode(encrypted.getBytes());
+ byte[] result = decrypt(rawKey, enc);
+ return new String(result);
+ }
+
+ private static byte[] getRawKey(byte[] seed) throws Exception {
+ KeyGenerator kgen = KeyGenerator.getInstance("AES");
+ SecureRandom sr = SecureRandom.getInstance("SHA1PRNG");
+ sr.setSeed(seed);
+ kgen.init(128, sr); // 192 and 256 bits may not be available
+ SecretKey skey = kgen.generateKey();
+ byte[] raw = skey.getEncoded();
+ return raw;
+ }
+
+
+ private static byte[] encrypt(byte[] raw, byte[] clear) throws Exception {
+ SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
+ Cipher cipher = Cipher.getInstance("AES");
+ cipher.init(Cipher.ENCRYPT_MODE, skeySpec);
+ byte[] encrypted = cipher.doFinal(clear);
+ return encrypted;
+ }
+
+ private static byte[] decrypt(byte[] raw, byte[] encrypted) throws Exception {
+ SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
+ Cipher cipher = Cipher.getInstance("AES");
+ cipher.init(Cipher.DECRYPT_MODE, skeySpec);
+ byte[] decrypted = cipher.doFinal(encrypted);
+ return decrypted;
+ }
+
+//
+// public static byte[] toByte(String hexString) {
+// int len = hexString.length()/2;
+// byte[] result = new byte[len];
+// for (int i = 0; i < len; i++)
+// result[i] = Integer.valueOf(hexString.substring(2*i, 2*i+2), 16).byteValue();
+// return result;
+// }
+//
+// public static String toHex(byte[] buf) {
+// if (buf == null)
+// return "";
+// StringBuffer result = new StringBuffer(2*buf.length);
+// for (int i = 0; i < buf.length; i++) {
+// appendHex(result, buf[i]);
+// }
+// return result.toString();
+// }
+// private final static String HEX = "0123456789ABCDEF";
+// private static void appendHex(StringBuffer sb, byte b) {
+// sb.append(HEX.charAt((b>>4)&0x0f)).append(HEX.charAt(b&0x0f));
+// }
+//
+}
+
diff --git a/src/com/fsck/k9/preferences/StorageExporter.java b/src/com/fsck/k9/preferences/StorageExporter.java
new file mode 100644
index 000000000..7d30f3618
--- /dev/null
+++ b/src/com/fsck/k9/preferences/StorageExporter.java
@@ -0,0 +1,81 @@
+package com.fsck.k9.preferences;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+public class StorageExporter
+{
+ //public static String VALIDITY = "K-9MailExport"; // Does outputting a fixed string in a known location make the encrypted data easier to break?
+ public static void exportPreferences(Context context, String uuid, String fileName, String encryptionKey) throws StorageImportExportException
+ {
+ try
+ {
+ Base64 base64 = new Base64();
+ File outFile = new File(fileName);
+ PrintWriter pf = new PrintWriter(outFile);
+ long keysEvaluated = 0;
+ long keysExported = 0;
+ pf.println("");
+
+ // String testval = SimpleCrypto.encrypt(encryptionKey, VALIDITY);
+
+ pf.print("");
+ Log.i(K9.LOG_TAG, "Exporting preferences for account " + uuid + " to file " + fileName);
+
+ SharedPreferences storage = Preferences.getPreferences(context).getPreferences();
+ Map prefs = storage.getAll();
+ for (Map.Entry entry : prefs.entrySet())
+ {
+ String key = entry.getKey();
+ String value = entry.getValue().toString();
+ //Log.i(K9.LOG_TAG, "Evaluating key " + key);
+ keysEvaluated++;
+ if (uuid != null)
+ {
+ String[] comps = key.split("\\.");
+ String keyUuid = comps[0];
+ //Log.i(K9.LOG_TAG, "Got key uuid " + keyUuid);
+ if (uuid.equals(keyUuid) == false)
+ {
+ //Log.i(K9.LOG_TAG, "Skipping key " + key + " which is for another account or global");
+ continue;
+ }
+ }
+ String keyEnc = SimpleCrypto.encrypt(encryptionKey, key, base64);
+ String valueEnc = SimpleCrypto.encrypt(encryptionKey, value, base64);
+ String output = keyEnc + ":" + valueEnc;
+ //Log.i(K9.LOG_TAG, "For key " + key + ", output is " + output);
+ pf.println(output);
+ keysExported++;
+
+ }
+
+ pf.println("");
+ pf.close();
+
+ Log.i(K9.LOG_TAG, "Exported " + keysExported + " settings of " + keysEvaluated
+ + " total for preferences for account " + uuid + " to file " + fileName + " which is size " + outFile.length());
+ }
+ catch (IOException ie)
+ {
+ throw new StorageImportExportException("Unable to export settings", ie);
+ }
+ catch (Exception e)
+ {
+ throw new StorageImportExportException("Unable to encrypt settings", e);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/StorageImportExportException.java b/src/com/fsck/k9/preferences/StorageImportExportException.java
new file mode 100644
index 000000000..979826468
--- /dev/null
+++ b/src/com/fsck/k9/preferences/StorageImportExportException.java
@@ -0,0 +1,26 @@
+package com.fsck.k9.preferences;
+
+public class StorageImportExportException extends Exception
+{
+
+ public StorageImportExportException()
+ {
+ super();
+ }
+
+ public StorageImportExportException(String detailMessage, Throwable throwable)
+ {
+ super(detailMessage, throwable);
+ }
+
+ public StorageImportExportException(String detailMessage)
+ {
+ super(detailMessage);
+ }
+
+ public StorageImportExportException(Throwable throwable)
+ {
+ super(throwable);
+ }
+
+}
diff --git a/src/com/fsck/k9/preferences/StorageImporter.java b/src/com/fsck/k9/preferences/StorageImporter.java
new file mode 100644
index 000000000..f77024335
--- /dev/null
+++ b/src/com/fsck/k9/preferences/StorageImporter.java
@@ -0,0 +1,167 @@
+package com.fsck.k9.preferences;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+
+import javax.xml.parsers.ParserConfigurationException;
+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 android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+
+public class StorageImporter
+{
+ public static int importPreferences(Context context, String fileName, String encryptionKey) throws StorageImportExportException
+ {
+ try
+ {
+ InputStream is = new FileInputStream(fileName);
+ return importPreferences(context, is, encryptionKey);
+ }
+ catch (FileNotFoundException fnfe)
+ {
+ throw new StorageImportExportException("Failure reading settings file " + fileName, fnfe);
+ }
+ }
+ public static int importPreferences(Context context, InputStream is, String encryptionKey) throws StorageImportExportException
+ {
+ try
+ {
+ Preferences preferences = Preferences.getPreferences(context);
+ SharedPreferences storage = preferences.getPreferences();
+ SharedPreferences.Editor editor = storage.edit();
+
+ SAXParserFactory spf = SAXParserFactory.newInstance();
+ SAXParser sp = spf.newSAXParser();
+ XMLReader xr = sp.getXMLReader();
+ StorageImporterHandler handler = new StorageImporterHandler();
+ xr.setContentHandler(handler);
+
+ xr.parse(new InputSource(is));
+ is.close();
+
+ Element dataset = handler.getRootElement();
+ String version = dataset.attributes.get("version");
+ Log.i(K9.LOG_TAG, "Got settings file version " + version);
+
+
+ IStorageImporter storageImporter = null;
+ if ("1".equals(version))
+ {
+ storageImporter = new StorageImporterVersion1();
+ }
+ else
+ {
+ throw new StorageImportExportException("Unable to read file of version " + version
+ + "; (only version 1 is readable)");
+ }
+ int numAccounts = 0;
+ if (storageImporter != null)
+ {
+ String data = dataset.data.toString();
+ numAccounts = storageImporter.importPreferences(preferences, editor, data, encryptionKey);
+ }
+ editor.commit();
+ Preferences.getPreferences(context).refreshAccounts();
+ return numAccounts;
+ }
+ catch (SAXException se)
+ {
+ throw new StorageImportExportException("Failure reading settings file", se);
+ }
+ catch (IOException ie)
+ {
+ throw new StorageImportExportException("Failure reading settings file", ie);
+ }
+ catch (ParserConfigurationException pce)
+ {
+ throw new StorageImportExportException("Failure reading settings file", pce);
+ }
+ }
+
+ private static class Element
+ {
+ String name;
+ Map attributes = new HashMap();
+ Map subElements = new HashMap();
+ StringBuilder data = new StringBuilder();
+ }
+
+ private static class StorageImporterHandler extends DefaultHandler
+ {
+ private Element rootElement = new Element();
+ private Stack mOpenTags = new Stack();
+
+ public Element getRootElement()
+ {
+ return this.rootElement;
+ }
+
+ @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);
+ Element element = new Element();
+ 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);
+ }
+ }
+
+ @Override
+ public void endElement(String namespaceURI, String localName, String qName)
+ {
+ Log.i(K9.LOG_TAG, "Ending element " + localName);
+ Element element = mOpenTags.pop();
+ Element superElement = mOpenTags.empty() ? null : mOpenTags.peek();
+ if (superElement != null)
+ {
+ superElement.subElements.put(element.name, element);
+ }
+ else
+ {
+ rootElement = element;
+ }
+ }
+
+ @Override
+ public void characters(char ch[], int start, int length)
+ {
+ String value = new String(ch, start, length);
+ mOpenTags.peek().data.append(value);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/StorageImporterVersion1.java b/src/com/fsck/k9/preferences/StorageImporterVersion1.java
new file mode 100644
index 000000000..6c2e23496
--- /dev/null
+++ b/src/com/fsck/k9/preferences/StorageImporterVersion1.java
@@ -0,0 +1,103 @@
+package com.fsck.k9.preferences;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.apache.commons.codec.binary.Base64;
+
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+
+public class StorageImporterVersion1 implements IStorageImporter
+{
+ public int importPreferences(Preferences preferences, SharedPreferences.Editor editor, String data, String encryptionKey) throws StorageImportExportException
+ {
+ try
+ {
+ Base64 base64 = new Base64();
+ List accountNumbers = Account.getExistingAccountNumbers(preferences);
+ Log.i(K9.LOG_TAG, "Existing accountNumbers = " + accountNumbers);
+ 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;
+ 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 = SimpleCrypto.decrypt(encryptionKey, keyEnc, base64);
+ String value = SimpleCrypto.decrypt(encryptionKey, valueEnc, base64);
+ 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);
+ }
+ }
+
+ } while (line != null);
+
+ editor.putString("accountUuids", accountUuids);
+ Log.i(K9.LOG_TAG, "Imported " + settingsImported + " settings and " + numAccounts + " accounts");
+ return numAccounts;
+ }
+ catch (IOException ie)
+ {
+ throw new StorageImportExportException("Unable to import settings", ie);
+ }
+ catch (Exception e)
+ {
+ throw new StorageImportExportException("Unable to decrypt settings", e);
+ }
+ }
+}