diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 9e3453ca0..d35644a58 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 719e4b15a..4bbacd1dc 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; @@ -435,6 +437,32 @@ public class Account implements BaseAccount { 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(); @@ -450,19 +478,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/K9.java b/src/com/fsck/k9/K9.java index 4628d9dc1..e08cb02fb 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -447,17 +447,9 @@ public class K9 extends Application { fontSizes.save(editor); } - @Override - public void onCreate() { - maybeSetupStrictMode(); - super.onCreate(); - app = this; - - - galleryBuggy = checkForBuggyGallery(); - - Preferences prefs = Preferences.getPreferences(this); + public static void loadPrefs(Preferences prefs) { SharedPreferences sprefs = prefs.getPreferences(); + DEBUG = sprefs.getBoolean("enableDebugLogging", false); DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); mAnimations = sprefs.getBoolean("animations", true); @@ -505,7 +497,18 @@ public class K9 extends Application { K9.setK9Language(sprefs.getString("language", "")); K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light)); + } + @Override + public void onCreate() { + maybeSetupStrictMode(); + super.onCreate(); + app = this; + + + galleryBuggy = checkForBuggyGallery(); + + loadPrefs(Preferences.getPreferences(this)); /* * We have to give MimeMessage a temp directory because File.createTempFile(String, String) * doesn't work in Android and MimeMessage does not have access to a Context. diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java index dece28412..4910150b8 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; @@ -29,7 +34,8 @@ public class Preferences { private Storage mStorage; - private List accounts; + private Map accounts = null; + private List accountsInOrder = null; private Account newAccount; private Context mContext; @@ -45,16 +51,35 @@ 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 { - accounts = new ArrayList(); } + if ((newAccount != null) && newAccount.getAccountNumber() != -1) { + newAccountMap.put(newAccount.getUuid(), newAccount); + accountsInOrder.add(newAccount); + newAccount = null; + } + + accounts = newAccountMap; } /** @@ -67,12 +92,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); } /** @@ -81,16 +101,9 @@ public class Preferences { * @return all accounts with {@link Account#isAvailable(Context)} */ 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)) { retval.add(account); } @@ -103,28 +116,22 @@ public class Preferences { if (accounts == null) { loadAccounts(); } + Account account = accounts.get(uuid); - for (Account account : accounts) { - if (account.getUuid().equals(uuid)) { - return account; - } - } - - if ((newAccount != null) && newAccount.getUuid().equals(uuid)) { - return newAccount; - } - - return null; + 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 025a95058..14d80b379 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -3,11 +3,13 @@ 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.Handler; import android.util.Log; @@ -29,6 +31,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; @@ -59,6 +63,8 @@ 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 { private void setViewTitle() { @@ -129,7 +135,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC }); } } - + + public void setProgress(boolean progress) + { + mHandler.progress(progress); + } + ActivityListener mListener = new ActivityListener() { @Override public void informUserOfStatus() { @@ -582,6 +593,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; } @@ -628,6 +642,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); } @@ -741,6 +761,97 @@ 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 { public AccountsAdapter(BaseAccount[] accounts) { diff --git a/src/com/fsck/k9/activity/ActivityListener.java b/src/com/fsck/k9/activity/ActivityListener.java index 432e87da2..732ffda92 100644 --- a/src/com/fsck/k9/activity/ActivityListener.java +++ b/src/com/fsck/k9/activity/ActivityListener.java @@ -204,7 +204,6 @@ public class ActivityListener extends MessagingListener { return mFolderCompleted; } - public int getFolderTotal() { return mFolderTotal; } diff --git a/src/com/fsck/k9/activity/AsyncUIProcessor.java b/src/com/fsck/k9/activity/AsyncUIProcessor.java new file mode 100644 index 000000000..4492b4869 --- /dev/null +++ b/src/com/fsck/k9/activity/AsyncUIProcessor.java @@ -0,0 +1,100 @@ +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..864689935 --- /dev/null +++ b/src/com/fsck/k9/activity/ExportHelper.java @@ -0,0 +1,54 @@ +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..6c2b6f3a6 --- /dev/null +++ b/src/com/fsck/k9/activity/ExportListener.java @@ -0,0 +1,8 @@ +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 55fc93076..b5900bc84 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -150,7 +150,12 @@ 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 @@ -538,7 +543,15 @@ public class FolderList extends K9ListActivity { onCompact(mAccount); return true; - + + case R.id.export: + onExport(mAccount); + return true; + + case R.id.export_all: + onExport(null); + return true; + case R.id.display_1st_class: { setDisplayMode(FolderMode.FIRST_CLASS); return true; diff --git a/src/com/fsck/k9/activity/ImportListener.java b/src/com/fsck/k9/activity/ImportListener.java new file mode 100644 index 000000000..442b612ee --- /dev/null +++ b/src/com/fsck/k9/activity/ImportListener.java @@ -0,0 +1,8 @@ +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 3fa72700b..d9b29817d 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; protected ScrollView mTopView; @@ -161,6 +163,11 @@ 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 204182be2..52887bd95 100644 --- a/src/com/fsck/k9/activity/K9ListActivity.java +++ b/src/com/fsck/k9/activity/K9ListActivity.java @@ -1,15 +1,17 @@ package com.fsck.k9.activity; import android.app.ListActivity; +import android.os.Bundle; import android.util.Log; import android.view.KeyEvent; import android.widget.AdapterView; import android.widget.ListView; -import android.os.Bundle; + +import com.fsck.k9.Account; import com.fsck.k9.K9; import com.fsck.k9.helper.DateFormatter; -public class K9ListActivity extends ListActivity { +public class K9ListActivity extends ListActivity implements Progressable { @Override public void onCreate(Bundle icicle) { K9Activity.setLanguage(this, K9.getK9Language()); @@ -87,4 +89,12 @@ 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 3627a9efe..edfc255d4 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -518,7 +518,12 @@ public class MessageList }); } } - + + public void setProgress(boolean progress) + { + mHandler.progress(progress); + } + public static void actionHandleFolder(Context context, Account account, String folder) { Intent intent = actionHandleFolderIntent(context, account, folder); context.startActivity(intent); @@ -1397,6 +1402,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..c24d23456 --- /dev/null +++ b/src/com/fsck/k9/activity/PasswordEntryDialog.java @@ -0,0 +1,78 @@ +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..5201e99b3 --- /dev/null +++ b/src/com/fsck/k9/activity/Progressable.java @@ -0,0 +1,5 @@ +package com.fsck.k9.activity; + +public interface Progressable { + public void setProgress(boolean progress); +} diff --git a/src/com/fsck/k9/helper/DateFormatter.java b/src/com/fsck/k9/helper/DateFormatter.java index 12a9e1a31..f964b2183 100644 --- a/src/com/fsck/k9/helper/DateFormatter.java +++ b/src/com/fsck/k9/helper/DateFormatter.java @@ -75,6 +75,10 @@ public class DateFormatter { return sChosenFormat; } + public static void clearChosenFormat() { + sChosenFormat = null; + } + public static DateFormat getDateFormat(Context context) { String formatString = getFormat(context); return getDateFormat(context, formatString); diff --git a/src/com/fsck/k9/preferences/IStorageImporter.java b/src/com/fsck/k9/preferences/IStorageImporter.java new file mode 100644 index 000000000..6702c4f80 --- /dev/null +++ b/src/com/fsck/k9/preferences/IStorageImporter.java @@ -0,0 +1,9 @@ +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..e53f5937d --- /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..870ef1b02 --- /dev/null +++ b/src/com/fsck/k9/preferences/StorageExporter.java @@ -0,0 +1,91 @@ +package com.fsck.k9.preferences; + +import java.io.File; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.commons.codec.binary.Base64; + +import com.fsck.k9.Account; +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); + + Preferences preferences = Preferences.getPreferences(context); + SharedPreferences storage = preferences.getPreferences(); + + Account[] accounts = preferences.getAccounts(); + Set accountUuids = new HashSet(); + for (Account account : accounts) { + accountUuids.add(account.getUuid()); + } + + Map < String, ? extends Object > prefs = storage.getAll(); + for (Map.Entry < String, ? extends Object > 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; + } + } else { + String[] comps = key.split("\\."); + if (comps.length > 1) { + String keyUuid = comps[0]; + if (accountUuids.contains(keyUuid) == false) { + Log.i(K9.LOG_TAG, "Skipping key " + key + " which is not for any current account"); + 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..bc2e10592 --- /dev/null +++ b/src/com/fsck/k9/preferences/StorageImportExportException.java @@ -0,0 +1,21 @@ +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..4991d266d --- /dev/null +++ b/src/com/fsck/k9/preferences/StorageImporter.java @@ -0,0 +1,141 @@ +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; +import com.fsck.k9.helper.DateFormatter; + +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(); + DateFormatter.clearChosenFormat(); + K9.loadPrefs(Preferences.getPreferences(context)); + 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..cfa2fc1f1 --- /dev/null +++ b/src/com/fsck/k9/preferences/StorageImporterVersion1.java @@ -0,0 +1,88 @@ +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); + } + } +}