From 6dc94fb78e068798709ada36e11159aa5461dab0 Mon Sep 17 00:00:00 2001 From: danapple Date: Sat, 26 Feb 2011 11:31:56 -0600 Subject: [PATCH] First mostly working copy of export/import. Committing while I sort out how to use git. --- AndroidManifest.xml | 6 +- res/layout/password_entry_dialog.xml | 21 +++ res/menu/accounts_context.xml | 13 +- res/menu/accounts_option.xml | 14 +- res/menu/folder_list_option.xml | 4 + res/menu/message_list_option.xml | 4 + res/values/strings.xml | 19 +- src/com/fsck/k9/Account.java | 53 ++++-- src/com/fsck/k9/Preferences.java | 84 ++++----- src/com/fsck/k9/activity/Accounts.java | 114 ++++++++++++ .../fsck/k9/activity/AsyncUIProcessor.java | 125 +++++++++++++ src/com/fsck/k9/activity/ExportHelper.java | 67 +++++++ src/com/fsck/k9/activity/ExportListener.java | 9 + src/com/fsck/k9/activity/FolderList.java | 12 +- src/com/fsck/k9/activity/ImportListener.java | 9 + src/com/fsck/k9/activity/K9Activity.java | 15 +- src/com/fsck/k9/activity/K9ListActivity.java | 17 +- src/com/fsck/k9/activity/MessageList.java | 14 +- .../fsck/k9/activity/PasswordEntryDialog.java | 90 ++++++++++ src/com/fsck/k9/activity/Progressable.java | 6 + .../fsck/k9/preferences/IStorageImporter.java | 10 ++ src/com/fsck/k9/preferences/SimpleCrypto.java | 90 ++++++++++ .../fsck/k9/preferences/StorageExporter.java | 81 +++++++++ .../StorageImportExportException.java | 26 +++ .../fsck/k9/preferences/StorageImporter.java | 167 ++++++++++++++++++ .../preferences/StorageImporterVersion1.java | 103 +++++++++++ 26 files changed, 1101 insertions(+), 72 deletions(-) create mode 100644 res/layout/password_entry_dialog.xml create mode 100644 src/com/fsck/k9/activity/AsyncUIProcessor.java create mode 100644 src/com/fsck/k9/activity/ExportHelper.java create mode 100644 src/com/fsck/k9/activity/ExportListener.java create mode 100644 src/com/fsck/k9/activity/ImportListener.java create mode 100644 src/com/fsck/k9/activity/PasswordEntryDialog.java create mode 100644 src/com/fsck/k9/activity/Progressable.java create mode 100644 src/com/fsck/k9/preferences/IStorageImporter.java create mode 100644 src/com/fsck/k9/preferences/SimpleCrypto.java create mode 100644 src/com/fsck/k9/preferences/StorageExporter.java create mode 100644 src/com/fsck/k9/preferences/StorageImportExportException.java create mode 100644 src/com/fsck/k9/preferences/StorageImporter.java create mode 100644 src/com/fsck/k9/preferences/StorageImporterVersion1.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ad061fc7a..86db7056f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -67,7 +67,7 @@ @@ -269,6 +269,10 @@ android:name="com.fsck.k9.activity.AccessibleEmailContentActivity" > + + + + + + diff --git a/res/menu/accounts_context.xml b/res/menu/accounts_context.xml index e55d13a94..afd94bcb4 100644 --- a/res/menu/accounts_context.xml +++ b/res/menu/accounts_context.xml @@ -6,8 +6,17 @@ android:title="@string/check_mail_action" /> - + + + + + + diff --git a/res/menu/accounts_option.xml b/res/menu/accounts_option.xml index 21b969c39..f03ef2c5b 100644 --- a/res/menu/accounts_option.xml +++ b/res/menu/accounts_option.xml @@ -31,8 +31,16 @@ --> - + android:icon="@android:drawable/ic_menu_preferences"> + + + + + + diff --git a/res/menu/folder_list_option.xml b/res/menu/folder_list_option.xml index 0e77995b1..d768acdf0 100644 --- a/res/menu/folder_list_option.xml +++ b/res/menu/folder_list_option.xml @@ -70,6 +70,10 @@ android:title="@string/global_settings_action" android:icon="@android:drawable/ic_menu_preferences" /> + + + + Search results Settings Open - Account settings - Folder settings - Global settings + Edit account settings + Edit folder settings + Edit global settings Remove account Clear pending actions (danger!) @@ -1028,4 +1028,17 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin » Unable to connect. + + Enter settings encryption password: + Export account settings + Export all settings + Import settings + Exporting settings... + Importing settings... + Exported settings to %s + Imported %s accounts from %s + Failed to export settings: %s + Failed from import settings from %s:%s + + diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index e70af5890..5e31f170e 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -21,7 +21,9 @@ import com.fsck.k9.view.ColorChip; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; +import java.util.Collections; import java.util.Date; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; @@ -478,7 +480,39 @@ public class Account implements BaseAccount deleteIdentities(preferences.getPreferences(), editor); editor.commit(); } - + + public static int findNewAccountNumber(List accountNumbers) + { + int newAccountNumber = -1; + Collections.sort(accountNumbers); + for (int accountNumber : accountNumbers) + { + if (accountNumber > newAccountNumber + 1) + { + break; + } + newAccountNumber = accountNumber; + } + newAccountNumber++; + return newAccountNumber; + } + + public static List getExistingAccountNumbers(Preferences preferences) + { + Account[] accounts = preferences.getAccounts(); + List accountNumbers = new LinkedList(); + for (int i = 0; i < accounts.length; i++) + { + accountNumbers.add(accounts[i].getAccountNumber()); + } + return accountNumbers; + } + public static int generateAccountNumber(Preferences preferences) + { + List accountNumbers = getExistingAccountNumbers(preferences); + return findNewAccountNumber(accountNumbers); + } + public synchronized void save(Preferences preferences) { SharedPreferences.Editor editor = preferences.getPreferences().edit(); @@ -496,22 +530,7 @@ public class Account implements BaseAccount * * I bet there is a much smarter way to do this. Anyone like to suggest it? */ - Account[] accounts = preferences.getAccounts(); - int[] accountNumbers = new int[accounts.length]; - for (int i = 0; i < accounts.length; i++) - { - accountNumbers[i] = accounts[i].getAccountNumber(); - } - Arrays.sort(accountNumbers); - for (int accountNumber : accountNumbers) - { - if (accountNumber > mAccountNumber + 1) - { - break; - } - mAccountNumber = accountNumber; - } - mAccountNumber++; + mAccountNumber = generateAccountNumber(preferences); String accountUuids = preferences.getPreferences().getString("accountUuids", ""); accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid; diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index 5769267f8..3f9ca57aa 100644 --- a/src/com/fsck/k9/Preferences.java +++ b/src/com/fsck/k9/Preferences.java @@ -3,7 +3,12 @@ package com.fsck.k9; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; import java.util.List; +import java.util.Map; + import android.content.Context; import android.content.SharedPreferences; import android.util.Config; @@ -32,7 +37,8 @@ public class Preferences private Storage mStorage; - private List accounts; + private Map accounts = null; + private List accountsInOrder = null; private Account newAccount; private Context mContext; @@ -51,22 +57,44 @@ public class Preferences private synchronized void loadAccounts() { + accounts = new HashMap(); + refreshAccounts(); + } + + public synchronized void refreshAccounts() + { + Map newAccountMap = new HashMap(); + accountsInOrder = new LinkedList(); String accountUuids = getPreferences().getString("accountUuids", null); if ((accountUuids != null) && (accountUuids.length() != 0)) { String[] uuids = accountUuids.split(","); - accounts = new ArrayList(uuids.length); for (String uuid : uuids) { - accounts.add(new Account(this, uuid)); + Account account = accounts.get(uuid); + if (account != null) + { + newAccountMap.put(uuid, account); + accountsInOrder.add(account); + } + else + { + Account newAccount = new Account(this, uuid); + newAccountMap.put(uuid, newAccount); + accountsInOrder.add(newAccount); + } } } - else + if ((newAccount != null) && newAccount.getAccountNumber() != -1) { - accounts = new ArrayList(); + newAccountMap.put(newAccount.getUuid(), newAccount); + accountsInOrder.add(newAccount); + newAccount = null; } - } + accounts = newAccountMap; + } + /** * Returns an array of the accounts on the system. If no accounts are * registered the method returns an empty array. @@ -79,13 +107,7 @@ public class Preferences loadAccounts(); } - if ((newAccount != null) && newAccount.getAccountNumber() != -1) - { - accounts.add(newAccount); - newAccount = null; - } - - return accounts.toArray(EMPTY_ACCOUNT_ARRAY); + return accountsInOrder.toArray(EMPTY_ACCOUNT_ARRAY); } /** @@ -95,18 +117,9 @@ public class Preferences */ public synchronized Collection getAvailableAccounts() { - if (accounts == null) - { - loadAccounts(); - } - - if ((newAccount != null) && newAccount.getAccountNumber() != -1) - { - accounts.add(newAccount); - newAccount = null; - } + Account[] allAccounts = getAccounts(); Collection retval = new ArrayList(accounts.size()); - for (Account account : accounts) + for (Account account : allAccounts) { if (account.isAvailable(mContext)) { @@ -123,33 +136,24 @@ public class Preferences { loadAccounts(); } - - for (Account account : accounts) - { - if (account.getUuid().equals(uuid)) - { - return account; - } - } - - if ((newAccount != null) && newAccount.getUuid().equals(uuid)) - { - return newAccount; - } - - return null; + Account account = accounts.get(uuid); + + return account; } public synchronized Account newAccount() { newAccount = new Account(K9.app); + accounts.put(newAccount.getUuid(), newAccount); + accountsInOrder.add(newAccount); return newAccount; } public synchronized void deleteAccount(Account account) { - accounts.remove(account); + accounts.remove(account.getUuid()); + accountsInOrder.remove(account); account.delete(this); if (newAccount == account) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 43396a848..15e1f9dda 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -3,12 +3,15 @@ package com.fsck.k9.activity; import android.app.AlertDialog; import android.app.Dialog; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; +import android.os.Environment; import android.os.Handler; import android.util.Log; import android.util.TypedValue; @@ -29,6 +32,8 @@ import com.fsck.k9.controller.MessagingListener; import com.fsck.k9.mail.Flag; import com.fsck.k9.view.ColorChip; +import java.io.FileNotFoundException; +import java.io.InputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -60,6 +65,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC private SearchAccount unreadAccount = null; private SearchAccount integratedInboxAccount = null; private FontSizes mFontSizes = K9.getFontSizes(); + + + private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1; class AccountsHandler extends Handler { @@ -153,6 +161,11 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC }); } } + + public void setProgress(boolean progress) + { + mHandler.progress(progress); + } ActivityListener mListener = new ActivityListener() { @@ -765,6 +778,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.recreate: onRecreate(realAccount); break; + case R.id.export: + onExport(realAccount); + break; } return true; } @@ -817,6 +833,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC case R.id.search: onSearchRequested(); break; + case R.id.export_all: + onExport(null); + break; + case R.id.import_settings: + onImport(); + break; default: return super.onOptionsItemSelected(item); } @@ -944,6 +966,98 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC } } } + } + + private void onImport() + { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.addCategory(Intent.CATEGORY_OPENABLE); + i.setType("*/*"); + startActivityForResult(Intent.createChooser(i, null), ACTIVITY_REQUEST_PICK_SETTINGS_FILE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) + { + Log.i(K9.LOG_TAG, "onActivityResult requestCode = " + requestCode + ", resultCode = " + resultCode + ", data = " + data); + if (resultCode != RESULT_OK) + return; + if (data == null) + { + return; + } + switch (requestCode) + { + case ACTIVITY_REQUEST_PICK_SETTINGS_FILE: + onImport(data.getData()); + break; + } + } + + + private void onImport(Uri uri) + { + Log.i(K9.LOG_TAG, "onImport importing from URI " + uri.getPath()); + try + { + final String fileName = uri.getPath(); + ContentResolver resolver = getContentResolver(); + final InputStream is = resolver.openInputStream(uri); + + PasswordEntryDialog dialog = new PasswordEntryDialog(this, getString(R.string.settings_encryption_password_prompt), + new PasswordEntryDialog.PasswordEntryListener() + { + public void passwordChosen(String chosenPassword) + { + String toastText = Accounts.this.getString(R.string.settings_importing ); + Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, Toast.LENGTH_SHORT); + toast.show(); + mHandler.progress(true); + AsyncUIProcessor.getInstance(Accounts.this.getApplication()).importSettings(is, chosenPassword, new ImportListener() + { + public void failure(final String message, Exception e) + { + Accounts.this.runOnUiThread(new Runnable() + { + public void run() + { + mHandler.progress(false); + String toastText = Accounts.this.getString(R.string.settings_import_failure, fileName, message ); + Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1); + toast.show(); + } + }); + } + + public void importSuccess(final int numAccounts) + { + Accounts.this.runOnUiThread(new Runnable() + { + public void run() + { + mHandler.progress(false); + String toastText = Accounts.this.getString(R.string.settings_import_success, numAccounts, fileName ); + Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1); + toast.show(); + refresh(); + } + }); + } + }); + } + + public void cancel() + { + } + }); + dialog.show(); + } + catch (FileNotFoundException fnfe) + { + String toastText = Accounts.this.getString(R.string.settings_import_failure, uri.getPath(), fnfe.getMessage() ); + Toast toast = Toast.makeText(Accounts.this.getApplication(), toastText, 1); + toast.show(); + } } class AccountsAdapter extends ArrayAdapter 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); + } + } +}