+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/menu/accounts_context.xml b/res/menu/accounts_context.xml
index 798d32d20..9eb952915 100644
--- a/res/menu/accounts_context.xml
+++ b/res/menu/accounts_context.xml
@@ -21,6 +21,8 @@
android:title="@string/remove_account_action" />
+
+ -
+
+
diff --git a/res/menu/disabled_accounts_context.xml b/res/menu/disabled_accounts_context.xml
new file mode 100644
index 000000000..54d6c2afc
--- /dev/null
+++ b/res/menu/disabled_accounts_context.xml
@@ -0,0 +1,19 @@
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index f1e86ad46..20cc791c5 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1051,6 +1051,46 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
›
Unable to connect.
+ Settings Import & Export
+ Export account settings
+ Export settings and accounts
+ Import
+ Export
+ Import settings
+ Import selection
+ Global settings
+ Exporting settings...
+ Importing settings...
+ Scanning file...
+ Saved exported settings to %s
+ Imported global settings from %s
+ Imported %s from %s
+
+ - 1 account
+ - %s accounts
+
+ Failed to export settings
+ Failed to import any settings from %s
+ Export succeeded
+ Export failed
+ Import succeeded
+ Import failed
+ Activate account
+ To be able to use the account \"%s\" you need to provide the %s.
+
+ - server password
+ - server passwords
+
+ Incoming server (%s):
+ Outgoing server (%s):
+
+ - Setting password...
+ - Setting passwords...
+
+ Activate
+
+ Unable to handle file of version %s
+
Account \"%s\" is unavailable; check storage
Save attachments to...
@@ -1059,5 +1099,6 @@ Welcome to K-9 Mail setup. K-9 is an open source mail client for Android origin
Move up
Move down
+ Moving account...
diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java
index f81fe794f..3a891c286 100644
--- a/src/com/fsck/k9/Account.java
+++ b/src/com/fsck/k9/Account.java
@@ -59,12 +59,21 @@ public class Account implements BaseAccount {
public static final String TYPE_OTHER = "OTHER";
private static final String[] networkTypes = { TYPE_WIFI, TYPE_MOBILE, TYPE_OTHER };
- private static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML;
- private static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false;
- private static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX;
- private static final String DEFAULT_QUOTE_PREFIX = ">";
- private static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true;
- private static final boolean DEFAULT_REPLY_AFTER_QUOTE = false;
+ public static final MessageFormat DEFAULT_MESSAGE_FORMAT = MessageFormat.HTML;
+ public static final boolean DEFAULT_MESSAGE_READ_RECEIPT = false;
+ public static final QuoteStyle DEFAULT_QUOTE_STYLE = QuoteStyle.PREFIX;
+ public static final String DEFAULT_QUOTE_PREFIX = ">";
+ public static final boolean DEFAULT_QUOTED_TEXT_SHOWN = true;
+ public static final boolean DEFAULT_REPLY_AFTER_QUOTE = false;
+
+ public static final String ACCOUNT_DESCRIPTION_KEY = "description";
+ public static final String STORE_URI_KEY = "storeUri";
+ public static final String TRANSPORT_URI_KEY = "transportUri";
+
+ public static final String IDENTITY_NAME_KEY = "name";
+ public static final String IDENTITY_EMAIL_KEY = "email";
+ public static final String IDENTITY_DESCRIPTION_KEY = "description";
+
/**
*
@@ -139,6 +148,16 @@ public class Account implements BaseAccount {
private CryptoProvider mCryptoProvider = null;
+ /**
+ * Indicates whether this account is enabled, i.e. ready for use, or not.
+ *
+ *
+ * Right now newly imported accounts are disabled if the settings file didn't contain a
+ * password for the incoming and/or outgoing server.
+ *
+ */
+ private boolean mEnabled;
+
/**
* Name of the folder that was last selected for a copy or move operation.
*
@@ -215,6 +234,7 @@ public class Account implements BaseAccount {
mSyncRemoteDeletions = true;
mCryptoApp = Apg.NAME;
mCryptoAutoSignature = false;
+ mEnabled = true;
searchableFolders = Searchable.ALL;
@@ -377,8 +397,9 @@ public class Account implements BaseAccount {
mCryptoApp = prefs.getString(mUuid + ".cryptoApp", Apg.NAME);
mCryptoAutoSignature = prefs.getBoolean(mUuid + ".cryptoAutoSignature", false);
+ mEnabled = prefs.getBoolean(mUuid + ".enabled", true);
}
-
+
protected synchronized void delete(Preferences preferences) {
String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(",");
String[] newUuids = new String[uuids.length - 1];
@@ -388,7 +409,7 @@ public class Account implements BaseAccount {
newUuids[i++] = uuid;
}
}
-
+
String accountUuids = Utility.combine(newUuids, ',');
SharedPreferences.Editor editor = preferences.getPreferences().edit();
editor.putString("accountUuids", accountUuids);
@@ -446,6 +467,7 @@ public class Account implements BaseAccount {
editor.remove(mUuid + ".replyAfterQuote");
editor.remove(mUuid + ".cryptoApp");
editor.remove(mUuid + ".cryptoAutoSignature");
+ editor.remove(mUuid + ".enabled");
editor.remove(mUuid + ".enableMoveButtons");
editor.remove(mUuid + ".hideMoveButtonsEnum");
for (String type : networkTypes) {
@@ -480,7 +502,7 @@ public class Account implements BaseAccount {
List accountNumbers = getExistingAccountNumbers(preferences);
return findNewAccountNumber(accountNumbers);
}
-
+
public void move(Preferences preferences, boolean moveUp) {
String[] uuids = preferences.getPreferences().getString("accountUuids", "").split(",");
SharedPreferences.Editor editor = preferences.getPreferences().edit();
@@ -489,7 +511,7 @@ public class Account implements BaseAccount {
for (int i = 0; i < uuids.length; i++) {
if (i > 0 && uuids[i].equals(mUuid)) {
newUuids[i] = newUuids[i-1];
- newUuids[i-1] = mUuid;
+ newUuids[i-1] = mUuid;
}
else {
newUuids[i] = uuids[i];
@@ -500,7 +522,7 @@ public class Account implements BaseAccount {
for (int i = uuids.length - 1; i >= 0; i--) {
if (i < uuids.length - 1 && uuids[i].equals(mUuid)) {
newUuids[i] = newUuids[i+1];
- newUuids[i+1] = mUuid;
+ newUuids[i+1] = mUuid;
}
else {
newUuids[i] = uuids[i];
@@ -510,9 +532,9 @@ public class Account implements BaseAccount {
String accountUuids = Utility.combine(newUuids, ',');
editor.putString("accountUuids", accountUuids);
editor.commit();
- preferences.refreshAccounts();
+ preferences.loadAccounts();
}
-
+
public synchronized void save(Preferences preferences) {
SharedPreferences.Editor editor = preferences.getPreferences().edit();
@@ -598,6 +620,7 @@ public class Account implements BaseAccount {
editor.putBoolean(mUuid + ".replyAfterQuote", mReplyAfterQuote);
editor.putString(mUuid + ".cryptoApp", mCryptoApp);
editor.putBoolean(mUuid + ".cryptoAutoSignature", mCryptoAutoSignature);
+ editor.putBoolean(mUuid + ".enabled", mEnabled);
editor.putBoolean(mUuid + ".vibrate", mNotificationSetting.shouldVibrate());
editor.putInt(mUuid + ".vibratePattern", mNotificationSetting.getVibratePattern());
@@ -1096,18 +1119,17 @@ public class Account implements BaseAccount {
return mUuid.hashCode();
}
-
private synchronized List loadIdentities(SharedPreferences prefs) {
List newIdentities = new ArrayList();
int ident = 0;
boolean gotOne = false;
do {
gotOne = false;
- String name = prefs.getString(mUuid + ".name." + ident, null);
- String email = prefs.getString(mUuid + ".email." + ident, null);
+ String name = prefs.getString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, null);
+ String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null);
boolean signatureUse = prefs.getBoolean(mUuid + ".signatureUse." + ident, true);
String signature = prefs.getString(mUuid + ".signature." + ident, null);
- String description = prefs.getString(mUuid + ".description." + ident, null);
+ String description = prefs.getString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, null);
final String replyTo = prefs.getString(mUuid + ".replyTo." + ident, null);
if (email != null) {
Identity identity = new Identity();
@@ -1145,13 +1167,13 @@ public class Account implements BaseAccount {
boolean gotOne = false;
do {
gotOne = false;
- String email = prefs.getString(mUuid + ".email." + ident, null);
+ String email = prefs.getString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, null);
if (email != null) {
- editor.remove(mUuid + ".name." + ident);
- editor.remove(mUuid + ".email." + ident);
+ editor.remove(mUuid + "." + IDENTITY_NAME_KEY + "." + ident);
+ editor.remove(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident);
editor.remove(mUuid + ".signatureUse." + ident);
editor.remove(mUuid + ".signature." + ident);
- editor.remove(mUuid + ".description." + ident);
+ editor.remove(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident);
editor.remove(mUuid + ".replyTo." + ident);
gotOne = true;
}
@@ -1164,11 +1186,11 @@ public class Account implements BaseAccount {
int ident = 0;
for (Identity identity : identities) {
- editor.putString(mUuid + ".name." + ident, identity.getName());
- editor.putString(mUuid + ".email." + ident, identity.getEmail());
+ editor.putString(mUuid + "." + IDENTITY_NAME_KEY + "." + ident, identity.getName());
+ editor.putString(mUuid + "." + IDENTITY_EMAIL_KEY + "." + ident, identity.getEmail());
editor.putBoolean(mUuid + ".signatureUse." + ident, identity.getSignatureUse());
editor.putString(mUuid + ".signature." + ident, identity.getSignature());
- editor.putString(mUuid + ".description." + ident, identity.getDescription());
+ editor.putString(mUuid + "." + IDENTITY_DESCRIPTION_KEY + "." + ident, identity.getDescription());
editor.putString(mUuid + ".replyTo." + ident, identity.getReplyTo());
ident++;
}
@@ -1460,4 +1482,11 @@ public class Account implements BaseAccount {
return StorageManager.getInstance(K9.app).isReady(localStorageProviderId);
}
+ public synchronized boolean isEnabled() {
+ return mEnabled;
+ }
+
+ public synchronized void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
}
diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java
index fd43cfc6a..6b8212295 100644
--- a/src/com/fsck/k9/K9.java
+++ b/src/com/fsck/k9/K9.java
@@ -463,59 +463,7 @@ public class K9 extends Application {
galleryBuggy = checkForBuggyGallery();
Preferences prefs = Preferences.getPreferences(this);
- SharedPreferences sprefs = prefs.getPreferences();
- DEBUG = sprefs.getBoolean("enableDebugLogging", false);
- DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false);
- mAnimations = sprefs.getBoolean("animations", true);
- mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true);
- mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false);
- mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false);
- mManageBack = sprefs.getBoolean("manageBack", false);
- mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false);
- mMeasureAccounts = sprefs.getBoolean("measureAccounts", true);
- mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true);
- mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false);
- mMessageListStars = sprefs.getBoolean("messageListStars", true);
- mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false);
- mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false);
- mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2);
-
- mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false);
- mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false);
-
- mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false);
- mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00");
- mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00");
-
- mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true);
- mShowContactName = sprefs.getBoolean("showContactName", false);
- mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false);
- mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f);
- mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false);
- mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false);
- mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false);
-
- useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy());
-
- mConfirmDelete = sprefs.getBoolean("confirmDelete", false);
- mConfirmSpam = sprefs.getBoolean("confirmSpam", false);
- mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true);
-
-
- mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false);
-
- compactLayouts = sprefs.getBoolean("compactLayouts", false);
- mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString());
- fontSizes.load(sprefs);
-
- try {
- setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED")));
- } catch (Exception e) {
- setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED);
- }
-
- K9.setK9Language(sprefs.getString("language", ""));
- K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light));
+ loadPrefs(prefs);
/*
* We have to give MimeMessage a temp directory because File.createTempFile(String, String)
@@ -587,6 +535,62 @@ public class K9 extends Application {
notifyObservers();
}
+ 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);
+ mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", true);
+ mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false);
+ mUseVolumeKeysForListNavigation = sprefs.getBoolean("useVolumeKeysForListNavigation", false);
+ mManageBack = sprefs.getBoolean("manageBack", false);
+ mStartIntegratedInbox = sprefs.getBoolean("startIntegratedInbox", false);
+ mMeasureAccounts = sprefs.getBoolean("measureAccounts", true);
+ mCountSearchMessages = sprefs.getBoolean("countSearchMessages", true);
+ mHideSpecialAccounts = sprefs.getBoolean("hideSpecialAccounts", false);
+ mMessageListStars = sprefs.getBoolean("messageListStars", true);
+ mMessageListCheckboxes = sprefs.getBoolean("messageListCheckboxes", false);
+ mMessageListTouchable = sprefs.getBoolean("messageListTouchable", false);
+ mMessageListPreviewLines = sprefs.getInt("messageListPreviewLines", 2);
+
+ mMobileOptimizedLayout = sprefs.getBoolean("mobileOptimizedLayout", false);
+ mZoomControlsEnabled = sprefs.getBoolean("zoomControlsEnabled", false);
+
+ mQuietTimeEnabled = sprefs.getBoolean("quietTimeEnabled", false);
+ mQuietTimeStarts = sprefs.getString("quietTimeStarts", "21:00");
+ mQuietTimeEnds = sprefs.getString("quietTimeEnds", "7:00");
+
+ mShowCorrespondentNames = sprefs.getBoolean("showCorrespondentNames", true);
+ mShowContactName = sprefs.getBoolean("showContactName", false);
+ mChangeContactNameColor = sprefs.getBoolean("changeRegisteredNameColor", false);
+ mContactNameColor = sprefs.getInt("registeredNameColor", 0xff00008f);
+ mMessageViewFixedWidthFont = sprefs.getBoolean("messageViewFixedWidthFont", false);
+ mMessageViewReturnToList = sprefs.getBoolean("messageViewReturnToList", false);
+ mMessageViewShowNext = sprefs.getBoolean("messageViewShowNext", false);
+
+ useGalleryBugWorkaround = sprefs.getBoolean("useGalleryBugWorkaround", K9.isGalleryBuggy());
+
+ mConfirmDelete = sprefs.getBoolean("confirmDelete", false);
+ mConfirmSpam = sprefs.getBoolean("confirmSpam", false);
+ mConfirmMarkAllAsRead = sprefs.getBoolean("confirmMarkAllAsRead", true);
+
+
+ mKeyguardPrivacy = sprefs.getBoolean("keyguardPrivacy", false);
+
+ compactLayouts = sprefs.getBoolean("compactLayouts", false);
+ mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString());
+ fontSizes.load(sprefs);
+
+ try {
+ setBackgroundOps(BACKGROUND_OPS.valueOf(sprefs.getString("backgroundOperations", "WHEN_CHECKED")));
+ } catch (Exception e) {
+ setBackgroundOps(BACKGROUND_OPS.WHEN_CHECKED);
+ }
+
+ K9.setK9Language(sprefs.getString("language", ""));
+ K9.setK9Theme(sprefs.getInt("theme", android.R.style.Theme_Light));
+ }
+
private void maybeSetupStrictMode() {
if (!K9.DEVELOPER_MODE)
return;
diff --git a/src/com/fsck/k9/Preferences.java b/src/com/fsck/k9/Preferences.java
index f30c7e578..cbaa8623b 100644
--- a/src/com/fsck/k9/Preferences.java
+++ b/src/com/fsck/k9/Preferences.java
@@ -49,36 +49,23 @@ public class Preferences {
}
}
- private synchronized void loadAccounts() {
+ public 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(",");
for (String uuid : uuids) {
- 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);
- }
+ Account newAccount = new Account(this, uuid);
+ accounts.put(uuid, newAccount);
+ accountsInOrder.add(newAccount);
}
}
if ((newAccount != null) && newAccount.getAccountNumber() != -1) {
- newAccountMap.put(newAccount.getUuid(), newAccount);
+ accounts.put(newAccount.getUuid(), newAccount);
accountsInOrder.add(newAccount);
newAccount = null;
}
-
- accounts = newAccountMap;
}
/**
@@ -103,7 +90,7 @@ public class Preferences {
Account[] allAccounts = getAccounts();
Collection retval = new ArrayList(accounts.size());
for (Account account : allAccounts) {
- if (account.isAvailable(mContext)) {
+ if (account.isEnabled() && account.isAvailable(mContext)) {
retval.add(account);
}
}
@@ -129,12 +116,12 @@ public class Preferences {
}
public synchronized void deleteAccount(Account account) {
- if (accounts != null) {
- accounts.remove(account.getUuid());
- }
- if (accountsInOrder != null) {
- accountsInOrder.remove(account);
- }
+ if (accounts != null) {
+ accounts.remove(account.getUuid());
+ }
+ if (accountsInOrder != null) {
+ accountsInOrder.remove(account);
+ }
account.delete(this);
diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index 4bc0d3291..45c622ee5 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -1,6 +1,9 @@
package com.fsck.k9.activity;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumSet;
@@ -10,16 +13,24 @@ import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
+import android.app.Activity;
import android.app.AlertDialog;
+import android.app.Application;
import android.app.Dialog;
+import android.app.ProgressDialog;
+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.text.Editable;
+import android.text.TextWatcher;
import android.util.Log;
+import android.util.SparseBooleanArray;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.Menu;
@@ -32,14 +43,22 @@ import android.view.View.OnClickListener;
import android.webkit.WebView;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CheckedTextView;
+import android.widget.CompoundButton;
+import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
+import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.RelativeLayout;
+import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.CompoundButton.OnCheckedChangeListener;
import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
@@ -50,6 +69,8 @@ import com.fsck.k9.Preferences;
import com.fsck.k9.R;
import com.fsck.k9.SearchAccount;
import com.fsck.k9.SearchSpecification;
+import com.fsck.k9.activity.misc.ExtendedAsyncTask;
+import com.fsck.k9.activity.misc.NonConfigurationInstance;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.AccountSetupBasics;
import com.fsck.k9.activity.setup.Prefs;
@@ -57,8 +78,21 @@ import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
import com.fsck.k9.helper.SizeFormatter;
import com.fsck.k9.mail.Flag;
+import com.fsck.k9.mail.ServerSettings;
+import com.fsck.k9.mail.Store;
+import com.fsck.k9.mail.Transport;
+import com.fsck.k9.mail.internet.MimeUtility;
import com.fsck.k9.mail.store.StorageManager;
+import com.fsck.k9.mail.store.WebDavStore;
import com.fsck.k9.view.ColorChip;
+import com.fsck.k9.preferences.SettingsExporter;
+import com.fsck.k9.preferences.SettingsImportExportException;
+import com.fsck.k9.preferences.SettingsImporter;
+import com.fsck.k9.preferences.SettingsImporter.AccountDescription;
+import com.fsck.k9.preferences.SettingsImporter.AccountDescriptionPair;
+import com.fsck.k9.preferences.SettingsImporter.ImportContents;
+import com.fsck.k9.preferences.SettingsImporter.ImportResults;
+
public class Accounts extends K9ListActivity implements OnItemClickListener, OnClickListener {
@@ -88,6 +122,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
private SearchAccount integratedInboxAccount = null;
private FontSizes mFontSizes = K9.getFontSizes();
+ /**
+ * Contains information about objects that need to be retained on configuration changes.
+ *
+ * @see #onRetainNonConfigurationInstance()
+ */
+ private NonConfigurationInstance mNonConfigurationInstance;
+
+
+ private static final int ACTIVITY_REQUEST_PICK_SETTINGS_FILE = 1;
+
class AccountsHandler extends Handler {
private void setViewTitle() {
String dispString = mListener.formatHeader(Accounts.this, getString(R.string.accounts_title), mUnreadMessageCount, getTimeFormat());
@@ -158,6 +202,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
}
+ public void setProgress(boolean progress) {
+ mHandler.progress(progress);
+ }
+
ActivityListener mListener = new ActivityListener() {
@Override
public void informUserOfStatus() {
@@ -256,6 +304,21 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
context.startActivity(intent);
}
+ @Override
+ public void onNewIntent(Intent intent) {
+ Uri uri = intent.getData();
+ Log.i(K9.LOG_TAG, "Accounts Activity got uri " + uri);
+ if (uri != null) {
+ ContentResolver contentResolver = getContentResolver();
+
+ Log.i(K9.LOG_TAG, "Accounts Activity got content of type " + contentResolver.getType(uri));
+
+ String contentType = contentResolver.getType(uri);
+ if (MimeUtility.K9_SETTINGS_MIME_TYPE.equals(contentType)) {
+ onImport(uri);
+ }
+ }
+ }
@Override
public void onCreate(Bundle icicle) {
@@ -273,32 +336,41 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
Account[] accounts = Preferences.getPreferences(this).getAccounts();
Intent intent = getIntent();
+ //onNewIntent(intent);
+
boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true);
if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) {
onOpenAccount(integratedInboxAccount);
finish();
+ return;
} else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) {
- // fall through to "else" if !onOpenAccount()
finish();
- } else {
- requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
- requestWindowFeature(Window.FEATURE_PROGRESS);
+ return;
+ }
- setContentView(R.layout.accounts);
- ListView listView = getListView();
- listView.setOnItemClickListener(this);
- listView.setItemsCanFocus(false);
- listView.setEmptyView(findViewById(R.id.empty));
- listView.setScrollingCacheEnabled(false);
- findViewById(R.id.next).setOnClickListener(this);
- registerForContextMenu(listView);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ requestWindowFeature(Window.FEATURE_PROGRESS);
- if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) {
- String accountUuid = icicle.getString("selectedContextAccount");
- mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid);
- }
+ setContentView(R.layout.accounts);
+ ListView listView = getListView();
+ listView.setOnItemClickListener(this);
+ listView.setItemsCanFocus(false);
+ listView.setEmptyView(findViewById(R.id.empty));
+ listView.setScrollingCacheEnabled(false);
+ findViewById(R.id.next).setOnClickListener(this);
+ registerForContextMenu(listView);
- restoreAccountStats(icicle);
+ if (icicle != null && icicle.containsKey(SELECTED_CONTEXT_ACCOUNT)) {
+ String accountUuid = icicle.getString("selectedContextAccount");
+ mSelectedContextAccount = Preferences.getPreferences(this).getAccount(accountUuid);
+ }
+
+ restoreAccountStats(icicle);
+
+ // Handle activity restarts because of a configuration change (e.g. rotating the screen)
+ mNonConfigurationInstance = (NonConfigurationInstance) getLastNonConfigurationInstance();
+ if (mNonConfigurationInstance != null) {
+ mNonConfigurationInstance.restore(this);
}
}
@@ -348,9 +420,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
super.onPause();
MessagingController.getInstance(getApplication()).removeListener(mListener);
StorageManager.getInstance(getApplication()).removeListener(storageListener);
-
}
-
+
+ /**
+ * Save the reference to a currently displayed dialog or a running AsyncTask (if available).
+ */
+ @Override
+ public Object onRetainNonConfigurationInstance() {
+ Object retain = null;
+ if (mNonConfigurationInstance != null && mNonConfigurationInstance.retain()) {
+ retain = mNonConfigurationInstance;
+ }
+ return retain;
+ }
+
private BaseAccount[] accounts = new BaseAccount[0];
private enum ACCOUNT_LOCATION {
TOP, MIDDLE, BOTTOM;
@@ -365,12 +448,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
if (accounts[accounts.length - 1].equals(account)) {
accountLocation.remove(ACCOUNT_LOCATION.MIDDLE);
accountLocation.add(ACCOUNT_LOCATION.BOTTOM);
- }
+ }
}
return accountLocation;
}
-
-
+
+
private void refresh() {
accounts = Preferences.getPreferences(this).getAccounts();
@@ -472,7 +555,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount);
} else {
Account realAccount = (Account)account;
- if (!realAccount.isAvailable(this)) {
+ if (!realAccount.isEnabled()) {
+ onActivateAccount(realAccount);
+ return false;
+ } else if (!realAccount.isAvailable(this)) {
String toastText = getString(R.string.account_unavailable, account.getDescription());
Toast toast = Toast.makeText(getApplication(), toastText, Toast.LENGTH_SHORT);
toast.show();
@@ -489,6 +575,319 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
return true;
}
+ private void onActivateAccount(Account account) {
+ List disabledAccounts = new ArrayList();
+ disabledAccounts.add(account);
+ promptForServerPasswords(disabledAccounts);
+ }
+
+ /**
+ * Ask the user to enter the server passwords for disabled accounts.
+ *
+ * @param disabledAccounts
+ * A non-empty list of {@link Account}s to ask the user for passwords. Never
+ * {@code null}.
+ * Note: Calling this method will modify the supplied list.
+ */
+ private void promptForServerPasswords(final List disabledAccounts) {
+ Account account = disabledAccounts.remove(0);
+ PasswordPromptDialog dialog = new PasswordPromptDialog(account, disabledAccounts);
+ setNonConfigurationInstance(dialog);
+ dialog.show(this);
+ }
+
+ /**
+ * Ask the user for the incoming/outgoing server passwords.
+ */
+ private static class PasswordPromptDialog implements NonConfigurationInstance, TextWatcher {
+ private AlertDialog mDialog;
+ private EditText mIncomingPasswordView;
+ private EditText mOutgoingPasswordView;
+ private CheckBox mUseIncomingView;
+
+ private Account mAccount;
+ private List mRemainingAccounts;
+ private String mIncomingPassword;
+ private String mOutgoingPassword;
+ private boolean mUseIncoming;
+
+ /**
+ * Constructor
+ *
+ * @param account
+ * The {@link Account} to ask the server passwords for. Never {@code null}.
+ * @param accounts
+ * The (possibly empty) list of remaining accounts to ask passwords for. Never
+ * {@code null}.
+ */
+ PasswordPromptDialog(Account account, List accounts) {
+ mAccount = account;
+ mRemainingAccounts = accounts;
+ }
+
+ @Override
+ public void restore(Activity activity) {
+ show((Accounts) activity, true);
+ }
+
+ @Override
+ public boolean retain() {
+ if (mDialog != null) {
+ // Retain entered passwords and checkbox state
+ mIncomingPassword = mIncomingPasswordView.getText().toString();
+ if (mOutgoingPasswordView != null) {
+ mOutgoingPassword = mOutgoingPasswordView.getText().toString();
+ mUseIncoming = mUseIncomingView.isChecked();
+ }
+
+ // Dismiss dialog
+ mDialog.dismiss();
+
+ // Clear all references to UI objects
+ mDialog = null;
+ mIncomingPasswordView = null;
+ mOutgoingPasswordView = null;
+ mUseIncomingView = null;
+ return true;
+ }
+ return false;
+ }
+
+ public void show(Accounts activity) {
+ show(activity, false);
+ }
+
+ private void show(final Accounts activity, boolean restore) {
+ ServerSettings incoming = Store.decodeStoreUri(mAccount.getStoreUri());
+ ServerSettings outgoing = Transport.decodeTransportUri(mAccount.getTransportUri());
+
+ // Don't ask for the password to the outgoing server for WebDAV accounts, because
+ // incoming and outgoing servers are identical for this account type.
+ boolean configureOutgoingServer = !WebDavStore.STORE_TYPE.equals(outgoing.type);
+
+ // Create a ScrollView that will be used as container for the whole layout
+ final ScrollView scrollView = new ScrollView(activity);
+
+ // Create the dialog
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(activity.getString(R.string.settings_import_activate_account_header));
+ builder.setView(scrollView);
+ builder.setPositiveButton(activity.getString(R.string.okay_action),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ String incomingPassword = mIncomingPasswordView.getText().toString();
+ String outgoingPassword = null;
+ if (mOutgoingPasswordView != null) {
+ outgoingPassword = (mUseIncomingView.isChecked()) ?
+ incomingPassword : mOutgoingPasswordView.getText().toString();
+ }
+
+ dialog.dismiss();
+
+ // Set the server passwords in the background
+ SetPasswordsAsyncTask asyncTask = new SetPasswordsAsyncTask(activity, mAccount,
+ incomingPassword, outgoingPassword, mRemainingAccounts);
+ activity.setNonConfigurationInstance(asyncTask);
+ asyncTask.execute();
+ }
+ });
+ builder.setNegativeButton(activity.getString(R.string.cancel_action),
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ activity.setNonConfigurationInstance(null);
+ }
+ });
+ mDialog = builder.create();
+
+ // Use the dialog's layout inflater so its theme is used (and not the activity's theme).
+ View layout = mDialog.getLayoutInflater().inflate(
+ R.layout.accounts_password_prompt, null);
+
+ // Set the intro text that tells the user what to do
+ TextView intro = (TextView) layout.findViewById(R.id.password_prompt_intro);
+ String serverPasswords = activity.getResources().getQuantityString(
+ R.plurals.settings_import_server_passwords,
+ (configureOutgoingServer) ? 2 : 1);
+ intro.setText(activity.getString(R.string.settings_import_activate_account_intro,
+ mAccount.getDescription(), serverPasswords));
+
+ // Display the hostname of the incoming server
+ TextView incomingText = (TextView) layout.findViewById(
+ R.id.password_prompt_incoming_server);
+ incomingText.setText(activity.getString(R.string.settings_import_incoming_server,
+ incoming.host));
+
+ mIncomingPasswordView = (EditText) layout.findViewById(R.id.incoming_server_password);
+ mIncomingPasswordView.addTextChangedListener(this);
+
+ if (configureOutgoingServer) {
+ // Display the hostname of the outgoing server
+ TextView outgoingText = (TextView) layout.findViewById(
+ R.id.password_prompt_outgoing_server);
+ outgoingText.setText(activity.getString(R.string.settings_import_outgoing_server,
+ outgoing.host));
+
+ mOutgoingPasswordView = (EditText) layout.findViewById(
+ R.id.outgoing_server_password);
+ mOutgoingPasswordView.addTextChangedListener(this);
+
+ mUseIncomingView = (CheckBox) layout.findViewById(
+ R.id.use_incoming_server_password);
+ mUseIncomingView.setChecked(true);
+ mUseIncomingView.setOnCheckedChangeListener(new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (isChecked) {
+ mOutgoingPasswordView.setText(null);
+ mOutgoingPasswordView.setEnabled(false);
+ } else {
+ mOutgoingPasswordView.setText(mIncomingPasswordView.getText());
+ mOutgoingPasswordView.setEnabled(true);
+ }
+ }
+ });
+ } else {
+ layout.findViewById(R.id.outgoing_server_prompt).setVisibility(View.GONE);
+ }
+
+ // Add the layout to the ScrollView
+ scrollView.addView(layout);
+
+ // Show the dialog
+ mDialog.show();
+
+ // Restore the contents of the password boxes and the checkbox (if the dialog was
+ // retained during a configuration change).
+ if (restore) {
+ mIncomingPasswordView.setText(mIncomingPassword);
+ if (configureOutgoingServer) {
+ mOutgoingPasswordView.setText(mOutgoingPassword);
+ mUseIncomingView.setChecked(mUseIncoming);
+ }
+ } else {
+ // Trigger afterTextChanged() being called
+ // Work around this bug: https://code.google.com/p/android/issues/detail?id=6360
+ mIncomingPasswordView.setText(mIncomingPasswordView.getText());
+ }
+ }
+
+ @Override
+ public void afterTextChanged(Editable arg0) {
+ boolean enable = false;
+ // Is the password box for the incoming server password empty?
+ if (mIncomingPasswordView.getText().length() > 0) {
+ // Do we need to check the outgoing server password box?
+ if (mOutgoingPasswordView == null) {
+ enable = true;
+ }
+ // If the checkbox to use the incoming server password is checked we need to make
+ // sure that the password box for the outgoing server isn't empty.
+ else if (mUseIncomingView.isChecked() ||
+ mOutgoingPasswordView.getText().length() > 0) {
+ enable = true;
+ }
+ }
+
+ // Disable "OK" button if the user hasn't specified all necessary passwords.
+ mDialog.getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(enable);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Not used
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ // Not used
+ }
+ }
+
+ /**
+ * Set the incoming/outgoing server password in the background.
+ */
+ private static class SetPasswordsAsyncTask extends ExtendedAsyncTask {
+ private Account mAccount;
+ private String mIncomingPassword;
+ private String mOutgoingPassword;
+ private List mRemainingAccounts;
+ private Application mApplication;
+
+ protected SetPasswordsAsyncTask(Activity activity, Account account,
+ String incomingPassword, String outgoingPassword,
+ List remainingAccounts) {
+ super(activity);
+ mAccount = account;
+ mIncomingPassword = incomingPassword;
+ mOutgoingPassword = outgoingPassword;
+ mRemainingAccounts = remainingAccounts;
+ mApplication = mActivity.getApplication();
+ }
+
+ @Override
+ protected void showProgressDialog() {
+ String title = mActivity.getString(R.string.settings_import_activate_account_header);
+ int passwordCount = (mOutgoingPassword == null) ? 1 : 2;
+ String message = mActivity.getResources().getQuantityString(
+ R.plurals.settings_import_setting_passwords, passwordCount);
+ mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ // Set incoming server password
+ String storeUri = mAccount.getStoreUri();
+ ServerSettings incoming = Store.decodeStoreUri(storeUri);
+ ServerSettings newIncoming = incoming.newPassword(mIncomingPassword);
+ String newStoreUri = Store.createStoreUri(newIncoming);
+ mAccount.setStoreUri(newStoreUri);
+
+ if (mOutgoingPassword != null) {
+ // Set outgoing server password
+ String transportUri = mAccount.getTransportUri();
+ ServerSettings outgoing = Transport.decodeTransportUri(transportUri);
+ ServerSettings newOutgoing = outgoing.newPassword(mOutgoingPassword);
+ String newTransportUri = Transport.createTransportUri(newOutgoing);
+ mAccount.setTransportUri(newTransportUri);
+ }
+
+ // Mark account as enabled
+ mAccount.setEnabled(true);
+
+ // Save the account settings
+ mAccount.save(Preferences.getPreferences(mContext));
+
+ // Start services if necessary
+ K9.setServicesEnabled(mContext);
+
+ // Get list of folders from remote server
+ MessagingController.getInstance(mApplication).listFolders(mAccount, true, null);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Something went while setting account passwords", e);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ Accounts activity = (Accounts) mActivity;
+
+ // Let the activity know that the background task is complete
+ activity.setNonConfigurationInstance(null);
+
+ activity.refresh();
+ removeProgressDialog();
+
+ if (mRemainingAccounts.size() > 0) {
+ activity.promptForServerPasswords(mRemainingAccounts);
+ }
+ }
+ }
+
public void onClick(View view) {
if (view.getId() == R.id.next) {
onAddNewAccount();
@@ -613,6 +1012,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
case R.id.open:
onOpenAccount(mSelectedContextAccount);
break;
+ case R.id.activate:
+ onActivateAccount(realAccount);
+ break;
case R.id.check_mail:
onCheckMail(realAccount);
break;
@@ -631,6 +1033,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
case R.id.recreate:
onRecreate(realAccount);
break;
+ case R.id.export:
+ onExport(false, realAccount);
+ break;
case R.id.move_up:
onMove(realAccount, true);
break;
@@ -656,22 +1061,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
showDialog(DIALOG_RECREATE_ACCOUNT);
}
private void onMove(final Account account, final boolean up) {
- mHandler.progress(true);
- AsyncUIProcessor.getInstance(getApplication()).execute(
- new Runnable()
- {
- @Override
- public void run() {
- account.move(Preferences.getPreferences(Accounts.this), up);
- runOnUiThread(new Runnable() {
- @Override
- public void run() {
- refresh();
- mHandler.progress(false);
- }
- });
- }
- });
+ MoveAccountAsyncTask asyncTask = new MoveAccountAsyncTask(this, account, up);
+ setNonConfigurationInstance(asyncTask);
+ asyncTask.execute();
}
public void onItemClick(AdapterView> parent, View view, int position, long id) {
@@ -700,6 +1092,12 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
case R.id.search:
onSearchRequested();
break;
+ case R.id.export_all:
+ onExport(true, null);
+ break;
+ case R.id.import_settings:
+ onImport();
+ break;
default:
return super.onOptionsItemSelected(item);
}
@@ -800,10 +1198,16 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
menu.setHeaderTitle(R.string.accounts_context_menu_title);
- getMenuInflater().inflate(R.menu.accounts_context, menu);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
BaseAccount account = mAdapter.getItem(info.position);
+
+ if ((account instanceof Account) && !((Account) account).isEnabled()) {
+ getMenuInflater().inflate(R.menu.disabled_accounts_context, menu);
+ } else {
+ getMenuInflater().inflate(R.menu.accounts_context, menu);
+ }
+
if (account instanceof SearchAccount) {
for (int i = 0; i < menu.size(); i++) {
MenuItem item = menu.getItem(i);
@@ -829,6 +1233,317 @@ 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(MimeUtility.K9_SETTINGS_MIME_TYPE);
+ 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) {
+ ListImportContentsAsyncTask asyncTask = new ListImportContentsAsyncTask(this, uri);
+ setNonConfigurationInstance(asyncTask);
+ asyncTask.execute();
+ }
+
+
+ private void showSimpleDialog(int headerRes, int messageRes, Object... args) {
+ SimpleDialog dialog = new SimpleDialog(headerRes, messageRes, args);
+ dialog.show(this);
+ setNonConfigurationInstance(dialog);
+ }
+
+ /**
+ * A simple dialog.
+ */
+ private static class SimpleDialog implements NonConfigurationInstance {
+ private final int mHeaderRes;
+ private final int mMessageRes;
+ private Object[] mArguments;
+ private Dialog mDialog;
+
+ SimpleDialog(int headerRes, int messageRes, Object... args) {
+ this.mHeaderRes = headerRes;
+ this.mMessageRes = messageRes;
+ this.mArguments = args;
+ }
+
+ @Override
+ public void restore(Activity activity) {
+ show((Accounts) activity);
+ }
+
+ @Override
+ public boolean retain() {
+ if (mDialog != null) {
+ mDialog.dismiss();
+ mDialog = null;
+ return true;
+ }
+ return false;
+ }
+
+ public void show(final Accounts activity) {
+ final String message = generateMessage(activity);
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(mHeaderRes);
+ builder.setMessage(message);
+ builder.setPositiveButton(R.string.okay_action,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ activity.setNonConfigurationInstance(null);
+ okayAction(activity);
+ }
+ });
+ mDialog = builder.show();
+ }
+
+ /**
+ * Returns the message the dialog should display.
+ *
+ * @param activity
+ * The {@code Activity} this dialog belongs to.
+ *
+ * @return The message the dialog should display
+ */
+ protected String generateMessage(Accounts activity) {
+ return activity.getString(mMessageRes, mArguments);
+ }
+
+ /**
+ * This method is called after the "OK" button was pressed.
+ *
+ * @param activity
+ * The {@code Activity} this dialog belongs to.
+ */
+ protected void okayAction(Accounts activity) {
+ // Do nothing
+ }
+ }
+
+ /**
+ * Shows a dialog that displays how many accounts were successfully imported.
+ *
+ * @param importResults
+ * The {@link ImportResults} instance returned by the {@link SettingsImporter}.
+ * @param filename
+ * The name of the settings file that was imported.
+ */
+ private void showAccountsImportedDialog(ImportResults importResults, String filename) {
+ AccountsImportedDialog dialog = new AccountsImportedDialog(importResults, filename);
+ dialog.show(this);
+ setNonConfigurationInstance(dialog);
+ }
+
+ /**
+ * A dialog that displays how many accounts were successfully imported.
+ */
+ private static class AccountsImportedDialog extends SimpleDialog {
+ private ImportResults mImportResults;
+ private String mFilename;
+
+ AccountsImportedDialog(ImportResults importResults, String filename) {
+ super(R.string.settings_import_success_header, R.string.settings_import_success);
+ mImportResults = importResults;
+ mFilename = filename;
+ }
+
+ @Override
+ protected String generateMessage(Accounts activity) {
+ //TODO: display names of imported accounts (name from file *and* possibly new name)
+
+ int imported = mImportResults.importedAccounts.size();
+ String accounts = activity.getResources().getQuantityString(
+ R.plurals.settings_import_success, imported, imported);
+ return activity.getString(R.string.settings_import_success, accounts, mFilename);
+ }
+
+ @Override
+ protected void okayAction(Accounts activity) {
+ Context context = activity.getApplicationContext();
+ Preferences preferences = Preferences.getPreferences(context);
+ List disabledAccounts = new ArrayList();
+ for (AccountDescriptionPair accountPair : mImportResults.importedAccounts) {
+ Account account = preferences.getAccount(accountPair.imported.uuid);
+ if (!account.isEnabled()) {
+ disabledAccounts.add(account);
+ }
+ }
+ activity.promptForServerPasswords(disabledAccounts);
+ }
+ }
+
+ /**
+ * Display a dialog that lets the user select which accounts to import from the settings file.
+ *
+ * @param importContents
+ * The {@link ImportContents} instance returned by
+ * {@link SettingsImporter#getImportStreamContents(InputStream)}
+ * @param uri
+ * The (content) URI of the settings file.
+ */
+ private void showImportSelectionDialog(ImportContents importContents, Uri uri) {
+ ImportSelectionDialog dialog = new ImportSelectionDialog(importContents, uri);
+ dialog.show(this);
+ setNonConfigurationInstance(dialog);
+ }
+
+ /**
+ * A dialog that lets the user select which accounts to import from the settings file.
+ */
+ private static class ImportSelectionDialog implements NonConfigurationInstance {
+ private ImportContents mImportContents;
+ private Uri mUri;
+ private Dialog mDialog;
+ private ListView mImportSelectionView;
+ private SparseBooleanArray mSelection;
+
+
+ ImportSelectionDialog(ImportContents importContents, Uri uri) {
+ mImportContents = importContents;
+ mUri = uri;
+ }
+
+ @Override
+ public void restore(Activity activity) {
+ show((Accounts) activity, mSelection);
+ }
+
+ @Override
+ public boolean retain() {
+ if (mDialog != null) {
+ // Save the selection state of each list item
+ mSelection = mImportSelectionView.getCheckedItemPositions();
+ mImportSelectionView = null;
+
+ mDialog.dismiss();
+ mDialog = null;
+ return true;
+ }
+ return false;
+ }
+
+ public void show(Accounts activity) {
+ show(activity, null);
+ }
+
+ public void show(final Accounts activity, SparseBooleanArray selection) {
+ final ListView importSelectionView = new ListView(activity);
+ mImportSelectionView = importSelectionView;
+ List contents = new ArrayList();
+
+ if (mImportContents.globalSettings) {
+ contents.add(activity.getString(R.string.settings_import_global_settings));
+ }
+
+ for (AccountDescription account : mImportContents.accounts) {
+ contents.add(account.name);
+ }
+
+ importSelectionView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
+ importSelectionView.setAdapter(new ArrayAdapter(activity,
+ android.R.layout.simple_list_item_checked, contents));
+ importSelectionView.setOnItemSelectedListener(new OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int pos, long id) {
+ CheckedTextView ctv = (CheckedTextView)view;
+ ctv.setChecked(!ctv.isChecked());
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> arg0) { /* Do nothing */ }
+ });
+
+ if (selection != null) {
+ for (int i = 0, end = contents.size(); i < end; i++) {
+ importSelectionView.setItemChecked(i, selection.get(i));
+ }
+ }
+
+ //TODO: listview header: "Please select the settings you wish to import"
+ //TODO: listview footer: "Select all" / "Select none" buttons?
+ //TODO: listview footer: "Overwrite existing accounts?" checkbox
+
+ final AlertDialog.Builder builder = new AlertDialog.Builder(activity);
+ builder.setTitle(activity.getString(R.string.settings_import_selection));
+ builder.setView(importSelectionView);
+ builder.setInverseBackgroundForced(true);
+ builder.setPositiveButton(R.string.okay_action,
+ new DialogInterface.OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ListAdapter adapter = importSelectionView.getAdapter();
+ int count = adapter.getCount();
+ SparseBooleanArray pos = importSelectionView.getCheckedItemPositions();
+
+ boolean includeGlobals = mImportContents.globalSettings ? pos.get(0) : false;
+ List accountUuids = new ArrayList();
+ int start = mImportContents.globalSettings ? 1 : 0;
+ for (int i = start; i < count; i++) {
+ if (pos.get(i)) {
+ accountUuids.add(mImportContents.accounts.get(i-start).uuid);
+ }
+ }
+
+ /*
+ * TODO: Think some more about this. Overwriting could change the store
+ * type. This requires some additional code in order to work smoothly
+ * while the app is running.
+ */
+ boolean overwrite = false;
+
+ dialog.dismiss();
+ activity.setNonConfigurationInstance(null);
+
+ ImportAsyncTask importAsyncTask = new ImportAsyncTask(activity,
+ includeGlobals, accountUuids, overwrite, mUri);
+ activity.setNonConfigurationInstance(importAsyncTask);
+ importAsyncTask.execute();
+ }
+ });
+ builder.setNegativeButton(R.string.cancel_action,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ activity.setNonConfigurationInstance(null);
+ }
+ });
+ mDialog = builder.show();
+ }
+ }
+
+ /**
+ * Set the {@code NonConfigurationInstance} this activity should retain on configuration
+ * changes.
+ *
+ * @param inst
+ * The {@link NonConfigurationInstance} that should be retained when
+ * {@link Accounts#onRetainNonConfigurationInstance()} is called.
+ */
+ private void setNonConfigurationInstance(NonConfigurationInstance inst) {
+ mNonConfigurationInstance = inst;
+ }
+
class AccountsAdapter extends ArrayAdapter {
public AccountsAdapter(BaseAccount[] accounts) {
super(Accounts.this, 0, accounts);
@@ -1026,4 +1741,246 @@ public class Accounts extends K9ListActivity implements OnItemClickListener, OnC
}
+ public void onExport(final boolean includeGlobals, final Account account) {
+
+ // TODO, prompt to allow a user to choose which accounts to export
+ Set accountUuids = null;
+ if (account != null) {
+ accountUuids = new HashSet();
+ accountUuids.add(account.getUuid());
+ }
+
+ ExportAsyncTask asyncTask = new ExportAsyncTask(this, includeGlobals, accountUuids);
+ setNonConfigurationInstance(asyncTask);
+ asyncTask.execute();
+ }
+
+ /**
+ * Handles exporting of global settings and/or accounts in a background thread.
+ */
+ private static class ExportAsyncTask extends ExtendedAsyncTask {
+ private boolean mIncludeGlobals;
+ private Set mAccountUuids;
+ private String mFileName;
+
+
+ private ExportAsyncTask(Accounts activity, boolean includeGlobals,
+ Set accountUuids) {
+ super(activity);
+ mIncludeGlobals = includeGlobals;
+ mAccountUuids = accountUuids;
+ }
+
+ @Override
+ protected void showProgressDialog() {
+ String title = mContext.getString(R.string.settings_export_dialog_title);
+ String message = mContext.getString(R.string.settings_exporting);
+ mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try {
+ mFileName = SettingsExporter.exportToFile(mContext, mIncludeGlobals,
+ mAccountUuids);
+ } catch (SettingsImportExportException e) {
+ Log.w(K9.LOG_TAG, "Exception during export", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ Accounts activity = (Accounts) mActivity;
+
+ // Let the activity know that the background task is complete
+ activity.setNonConfigurationInstance(null);
+
+ removeProgressDialog();
+
+ if (success) {
+ activity.showSimpleDialog(R.string.settings_export_success_header,
+ R.string.settings_export_success, mFileName);
+ } else {
+ //TODO: better error messages
+ activity.showSimpleDialog(R.string.settings_export_failed_header,
+ R.string.settings_export_failure);
+ }
+ }
+ }
+
+ /**
+ * Handles importing of global settings and/or accounts in a background thread.
+ */
+ private static class ImportAsyncTask extends ExtendedAsyncTask {
+ private boolean mIncludeGlobals;
+ private List mAccountUuids;
+ private boolean mOverwrite;
+ private Uri mUri;
+ private ImportResults mImportResults;
+
+ private ImportAsyncTask(Accounts activity, boolean includeGlobals,
+ List accountUuids, boolean overwrite, Uri uri) {
+ super(activity);
+ mIncludeGlobals = includeGlobals;
+ mAccountUuids = accountUuids;
+ mOverwrite = overwrite;
+ mUri = uri;
+ }
+
+ @Override
+ protected void showProgressDialog() {
+ String title = mContext.getString(R.string.settings_import_dialog_title);
+ String message = mContext.getString(R.string.settings_importing);
+ mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try {
+ InputStream is = mContext.getContentResolver().openInputStream(mUri);
+ try {
+ mImportResults = SettingsImporter.importSettings(mContext, is,
+ mIncludeGlobals, mAccountUuids, mOverwrite);
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) { /* Ignore */ }
+ }
+ } catch (SettingsImportExportException e) {
+ Log.w(K9.LOG_TAG, "Exception during import", e);
+ return false;
+ } catch (FileNotFoundException e) {
+ Log.w(K9.LOG_TAG, "Couldn't open import file", e);
+ return false;
+ } catch (Exception e) {
+ Log.w(K9.LOG_TAG, "Unknown error", e);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ Accounts activity = (Accounts) mActivity;
+
+ // Let the activity know that the background task is complete
+ activity.setNonConfigurationInstance(null);
+
+ removeProgressDialog();
+
+ String filename = mUri.getLastPathSegment();
+ boolean globalSettings = mImportResults.globalSettings;
+ int imported = mImportResults.importedAccounts.size();
+ if (success && (globalSettings || imported > 0)) {
+ if (imported == 0) {
+ activity.showSimpleDialog(R.string.settings_import_success_header,
+ R.string.settings_import_global_settings_success, filename);
+ } else {
+ activity.showAccountsImportedDialog(mImportResults, filename);
+ }
+
+ activity.refresh();
+ } else {
+ //TODO: better error messages
+ activity.showSimpleDialog(R.string.settings_import_failed_header,
+ R.string.settings_import_failure, filename);
+ }
+ }
+ }
+
+ private static class ListImportContentsAsyncTask extends ExtendedAsyncTask {
+ private Uri mUri;
+ private ImportContents mImportContents;
+
+ private ListImportContentsAsyncTask(Accounts activity, Uri uri) {
+ super(activity);
+
+ mUri = uri;
+ }
+
+ @Override
+ protected void showProgressDialog() {
+ String title = mContext.getString(R.string.settings_import_dialog_title);
+ String message = mContext.getString(R.string.settings_import_scanning_file);
+ mProgressDialog = ProgressDialog.show(mActivity, title, message, true);
+ }
+
+ @Override
+ protected Boolean doInBackground(Void... params) {
+ try {
+ ContentResolver resolver = mContext.getContentResolver();
+ InputStream is = resolver.openInputStream(mUri);
+ try {
+ mImportContents = SettingsImporter.getImportStreamContents(is);
+ } finally {
+ try {
+ is.close();
+ } catch (IOException e) { /* Ignore */ }
+ }
+ } catch (SettingsImportExportException e) {
+ Log.w(K9.LOG_TAG, "Exception during export", e);
+ return false;
+ }
+ catch (FileNotFoundException e) {
+ Log.w(K9.LOG_TAG, "Couldn't read content from URI " + mUri);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onPostExecute(Boolean success) {
+ Accounts activity = (Accounts) mActivity;
+
+ // Let the activity know that the background task is complete
+ activity.setNonConfigurationInstance(null);
+
+ removeProgressDialog();
+
+ if (success) {
+ activity.showImportSelectionDialog(mImportContents, mUri);
+ } else {
+ String filename = mUri.getLastPathSegment();
+ //TODO: better error messages
+ activity.showSimpleDialog(R.string.settings_import_failed_header,
+ R.string.settings_import_failure, filename);
+ }
+ }
+ }
+
+ private static class MoveAccountAsyncTask extends ExtendedAsyncTask {
+ private Account mAccount;
+ private boolean mUp;
+
+ protected MoveAccountAsyncTask(Activity activity, Account account, boolean up) {
+ super(activity);
+ mAccount = account;
+ mUp = up;
+ }
+
+ @Override
+ protected void showProgressDialog() {
+ String message = mActivity.getString(R.string.manage_accounts_moving_message);
+ mProgressDialog = ProgressDialog.show(mActivity, null, message, true);
+ }
+
+ @Override
+ protected Void doInBackground(Void... args) {
+ mAccount.move(Preferences.getPreferences(mContext), mUp);
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void arg) {
+ Accounts activity = (Accounts) mActivity;
+
+ // Let the activity know that the background task is complete
+ activity.setNonConfigurationInstance(null);
+
+ activity.refresh();
+ removeProgressDialog();
+ }
+ }
}
diff --git a/src/com/fsck/k9/activity/AsyncUIProcessor.java b/src/com/fsck/k9/activity/AsyncUIProcessor.java
deleted file mode 100644
index 55947b2c4..000000000
--- a/src/com/fsck/k9/activity/AsyncUIProcessor.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package com.fsck.k9.activity;
-
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-
-import android.app.Application;
-
-/**
- * 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. There is no wakelock used here. Any network activity, or
- * true background activity, that is invoked from here should wakelock itself. UI-centric
- * activity does not need to be wakelocked, as it will simply continue when the phone wakes
- * without disruption.
- *
- */
-public class AsyncUIProcessor {
-
- private final ExecutorService threadPool = Executors.newCachedThreadPool();
- private static AsyncUIProcessor inst = null;
- private AsyncUIProcessor() {
- }
-
- public synchronized static AsyncUIProcessor getInstance(Application application) {
- if (inst == null) {
- inst = new AsyncUIProcessor();
- }
- return inst;
- }
- public void execute(Runnable runnable) {
- threadPool.execute(runnable);
- }
-}
diff --git a/src/com/fsck/k9/activity/ChooseAccount.java b/src/com/fsck/k9/activity/ChooseAccount.java
index 6b04f6b76..0c1c0041c 100644
--- a/src/com/fsck/k9/activity/ChooseAccount.java
+++ b/src/com/fsck/k9/activity/ChooseAccount.java
@@ -28,6 +28,7 @@ import java.util.List;
* @see K9ExpandableListActivity
*/
public class ChooseAccount extends K9ExpandableListActivity {
+ private static final Account[] EMPTY_ACCOUNT_ARRAY = new Account[0];
/**
* {@link Intent} extended data name for storing {@link Account#getUuid()
@@ -50,7 +51,7 @@ public class ChooseAccount extends K9ExpandableListActivity {
final ExpandableListView expandableListView = getExpandableListView();
expandableListView.setItemsCanFocus(false);
- final ExpandableListAdapter adapter = createAdapter();
+ final IdentitiesAdapter adapter = createAdapter();
setListAdapter(adapter);
expandableListView.setOnChildClickListener(new ExpandableListView.OnChildClickListener() {
@@ -77,7 +78,7 @@ public class ChooseAccount extends K9ExpandableListActivity {
final Bundle extras = getIntent().getExtras();
final String uuid = extras.getString(EXTRA_ACCOUNT);
if (uuid != null) {
- final Account[] accounts = Preferences.getPreferences(this).getAccounts();
+ final Account[] accounts = adapter.getAccounts();
final int length = accounts.length;
for (int i = 0; i < length; i++) {
final Account account = accounts[i];
@@ -106,7 +107,7 @@ public class ChooseAccount extends K9ExpandableListActivity {
}
}
- private ExpandableListAdapter createAdapter() {
+ private IdentitiesAdapter createAdapter() {
return new IdentitiesAdapter(this, getLayoutInflater());
}
@@ -123,10 +124,13 @@ public class ChooseAccount extends K9ExpandableListActivity {
private Context mContext;
private LayoutInflater mLayoutInflater;
+ private Account[] mAccounts;
public IdentitiesAdapter(final Context context, final LayoutInflater layoutInflater) {
mContext = context;
mLayoutInflater = layoutInflater;
+ Preferences prefs = Preferences.getPreferences(mContext);
+ mAccounts = prefs.getAvailableAccounts().toArray(EMPTY_ACCOUNT_ARRAY);
}
@Override
@@ -233,7 +237,7 @@ public class ChooseAccount extends K9ExpandableListActivity {
}
private Account[] getAccounts() {
- return Preferences.getPreferences(mContext).getAccounts();
+ return mAccounts;
}
}
}
diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index c161e23e5..47b346fcc 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -152,6 +152,7 @@ public class FolderList extends K9ListActivity {
}
}
+
/**
* 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
diff --git a/src/com/fsck/k9/activity/K9Activity.java b/src/com/fsck/k9/activity/K9Activity.java
index b2c1827c6..263324671 100644
--- a/src/com/fsck/k9/activity/K9Activity.java
+++ b/src/com/fsck/k9/activity/K9Activity.java
@@ -195,6 +195,4 @@ public class K9Activity extends Activity {
return false;
}
}
-
-
}
diff --git a/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java
new file mode 100644
index 000000000..f8bef5741
--- /dev/null
+++ b/src/com/fsck/k9/activity/misc/ExtendedAsyncTask.java
@@ -0,0 +1,111 @@
+package com.fsck.k9.activity.misc;
+
+import android.app.Activity;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.os.AsyncTask;
+
+/**
+ * Extends {@link AsyncTask} with methods to attach and detach an {@link Activity}.
+ *
+ *
+ * This is necessary to properly handle configuration changes that will restart an activity.
+ *
+ * Note:
+ * Implementing classes need to make sure they have no reference to the {@code Activity} instance
+ * that created the instance of that class. So if it's implemented as inner class, it needs to be
+ * {@code static}.
+ *
+ *
+ * @param
+ * see {@link AsyncTask}
+ * @param
*/
public class ImapStore extends Store {
+ public static final String STORE_TYPE = "IMAP";
+
public static final int CONNECTION_SECURITY_NONE = 0;
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
@@ -131,6 +136,191 @@ public class ImapStore extends Store {
private static final String[] EMPTY_STRING_ARRAY = new String[0];
+ /**
+ * Decodes an ImapStore URI.
+ *
+ * Possible forms:
+ *
+ * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
+ * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+ * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+ * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+ * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ *
+ */
+ public static ImapStoreSettings decodeUri(String uri) {
+ String host;
+ int port;
+ ConnectionSecurity connectionSecurity;
+ String authenticationType = null;
+ String username = null;
+ String password = null;
+ String pathPrefix = null;
+
+ URI imapUri;
+ try {
+ imapUri = new URI(uri);
+ } catch (URISyntaxException use) {
+ throw new IllegalArgumentException("Invalid ImapStore URI", use);
+ }
+
+ String scheme = imapUri.getScheme();
+ if (scheme.equals("imap")) {
+ connectionSecurity = ConnectionSecurity.NONE;
+ port = 143;
+ } else if (scheme.equals("imap+tls")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
+ port = 143;
+ } else if (scheme.equals("imap+tls+")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
+ port = 143;
+ } else if (scheme.equals("imap+ssl+")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
+ port = 993;
+ } else if (scheme.equals("imap+ssl")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
+ port = 993;
+ } else {
+ throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
+ }
+
+ host = imapUri.getHost();
+
+ if (imapUri.getPort() != -1) {
+ port = imapUri.getPort();
+ }
+
+ if (imapUri.getUserInfo() != null) {
+ try {
+ String userinfo = imapUri.getUserInfo();
+ String[] userInfoParts = userinfo.split(":");
+
+ if (userinfo.endsWith(":")) {
+ // Password is empty. This can only happen after an account was imported.
+ authenticationType = AuthType.valueOf(userInfoParts[0]).name();
+ username = URLDecoder.decode(userInfoParts[1], "UTF-8");
+ } else if (userInfoParts.length == 2) {
+ authenticationType = AuthType.PLAIN.name();
+ username = URLDecoder.decode(userInfoParts[0], "UTF-8");
+ password = URLDecoder.decode(userInfoParts[1], "UTF-8");
+ } else {
+ authenticationType = AuthType.valueOf(userInfoParts[0]).name();
+ username = URLDecoder.decode(userInfoParts[1], "UTF-8");
+ password = URLDecoder.decode(userInfoParts[2], "UTF-8");
+ }
+ } catch (UnsupportedEncodingException enc) {
+ // This shouldn't happen since the encoding is hardcoded to UTF-8
+ throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
+ }
+ }
+
+ String path = imapUri.getPath();
+ if (path != null && path.length() > 0) {
+ pathPrefix = path.substring(1);
+ if (pathPrefix != null && pathPrefix.trim().length() == 0) {
+ pathPrefix = null;
+ }
+ }
+
+ return new ImapStoreSettings(host, port, connectionSecurity, authenticationType, username,
+ password, pathPrefix);
+ }
+
+ /**
+ * Creates an ImapStore URI with the supplied settings.
+ *
+ * @param server
+ * The {@link ServerSettings} object that holds the server settings.
+ *
+ * @return An ImapStore URI that holds the same information as the {@code server} parameter.
+ *
+ * @see Account#getStoreUri()
+ * @see ImapStore#decodeUri(String)
+ */
+ public static String createUri(ServerSettings server) {
+ String userEnc;
+ String passwordEnc;
+ try {
+ userEnc = URLEncoder.encode(server.username, "UTF-8");
+ passwordEnc = (server.password != null) ?
+ URLEncoder.encode(server.password, "UTF-8") : "";
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Could not encode username or password", e);
+ }
+
+ String scheme;
+ switch (server.connectionSecurity) {
+ case SSL_TLS_OPTIONAL:
+ scheme = "imap+ssl";
+ break;
+ case SSL_TLS_REQUIRED:
+ scheme = "imap+ssl+";
+ break;
+ case STARTTLS_OPTIONAL:
+ scheme = "imap+tls";
+ break;
+ case STARTTLS_REQUIRED:
+ scheme = "imap+tls+";
+ break;
+ default:
+ case NONE:
+ scheme = "imap";
+ break;
+ }
+
+ AuthType authType;
+ try {
+ authType = AuthType.valueOf(server.authenticationType);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid authentication type: " +
+ server.authenticationType);
+ }
+
+ String userInfo = authType.toString() + ":" + userEnc + ":" + passwordEnc;
+ try {
+ Map extra = server.getExtra();
+ String prefix = (extra != null) ? extra.get(ImapStoreSettings.PATH_PREFIX_KEY) : null;
+ return new URI(scheme, userInfo, server.host, server.port,
+ prefix,
+ null, null).toString();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Can't create ImapStore URI", e);
+ }
+ }
+
+ /**
+ * This class is used to store the decoded contents of an ImapStore URI.
+ *
+ * @see ImapStore#decodeUri(String)
+ */
+ private static class ImapStoreSettings extends ServerSettings {
+ private static final String PATH_PREFIX_KEY = "pathPrefix";
+
+ public final String pathPrefix;
+
+ protected ImapStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
+ String authenticationType, String username, String password, String pathPrefix) {
+ super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
+ password);
+ this.pathPrefix = pathPrefix;
+ }
+
+ @Override
+ public Map getExtra() {
+ Map extra = new HashMap();
+ putIfNotNull(extra, PATH_PREFIX_KEY, pathPrefix);
+ return extra;
+ }
+
+ @Override
+ public ServerSettings newPassword(String newPassword) {
+ return new ImapStoreSettings(host, port, connectionSecurity, authenticationType,
+ username, newPassword, pathPrefix);
+ }
+ }
+
+
private String mHost;
private int mPort;
private String mUsername;
@@ -228,74 +418,42 @@ public class ImapStore extends Store {
*/
private HashMap mFolderCache = new HashMap();
- /**
- * imap://auth:user:password@server:port CONNECTION_SECURITY_NONE
- * imap+tls://auth:user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * imap+tls+://auth:user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * imap+ssl+://auth:user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * imap+ssl://auth:user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
- *
- * @param _uri
- */
public ImapStore(Account account) throws MessagingException {
super(account);
- URI uri;
+
+ ImapStoreSettings settings;
try {
- uri = new URI(mAccount.getStoreUri());
- } catch (URISyntaxException use) {
- throw new MessagingException("Invalid ImapStore URI", use);
+ settings = decodeUri(mAccount.getStoreUri());
+ } catch (IllegalArgumentException e) {
+ throw new MessagingException("Error while decoding store URI", e);
}
- String scheme = uri.getScheme();
- if (scheme.equals("imap")) {
+ mHost = settings.host;
+ mPort = settings.port;
+
+ switch (settings.connectionSecurity) {
+ case NONE:
mConnectionSecurity = CONNECTION_SECURITY_NONE;
- mPort = 143;
- } else if (scheme.equals("imap+tls")) {
+ break;
+ case STARTTLS_OPTIONAL:
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- mPort = 143;
- } else if (scheme.equals("imap+tls+")) {
+ break;
+ case STARTTLS_REQUIRED:
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- mPort = 143;
- } else if (scheme.equals("imap+ssl+")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- mPort = 993;
- } else if (scheme.equals("imap+ssl")) {
+ break;
+ case SSL_TLS_OPTIONAL:
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- mPort = 993;
- } else {
- throw new MessagingException("Unsupported protocol");
+ break;
+ case SSL_TLS_REQUIRED:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
+ break;
}
- mHost = uri.getHost();
+ mAuthType = AuthType.valueOf(settings.authenticationType);
+ mUsername = settings.username;
+ mPassword = settings.password;
- if (uri.getPort() != -1) {
- mPort = uri.getPort();
- }
-
- if (uri.getUserInfo() != null) {
- try {
- String[] userInfoParts = uri.getUserInfo().split(":");
- if (userInfoParts.length == 2) {
- mAuthType = AuthType.PLAIN;
- mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8");
- mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8");
- } else {
- mAuthType = AuthType.valueOf(userInfoParts[0]);
- mUsername = URLDecoder.decode(userInfoParts[1], "UTF-8");
- mPassword = URLDecoder.decode(userInfoParts[2], "UTF-8");
- }
- } catch (UnsupportedEncodingException enc) {
- // This shouldn't happen since the encoding is hardcoded to UTF-8
- Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc);
- }
- }
-
- if ((uri.getPath() != null) && (uri.getPath().length() > 0)) {
- mPathPrefix = uri.getPath().substring(1);
- if (mPathPrefix != null && mPathPrefix.trim().length() == 0) {
- mPathPrefix = null;
- }
- }
+ mPathPrefix = settings.pathPrefix;
mModifiedUtf7Charset = new CharsetProvider().charsetForName("X-RFC-3501");
}
diff --git a/src/com/fsck/k9/mail/store/Pop3Store.java b/src/com/fsck/k9/mail/store/Pop3Store.java
index 0ce234fc0..56d9b5fd8 100644
--- a/src/com/fsck/k9/mail/store/Pop3Store.java
+++ b/src/com/fsck/k9/mail/store/Pop3Store.java
@@ -26,12 +26,19 @@ import java.util.HashSet;
import java.util.List;
public class Pop3Store extends Store {
+ public static final String STORE_TYPE = "POP3";
+
public static final int CONNECTION_SECURITY_NONE = 0;
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
+ private enum AuthType {
+ PLAIN,
+ CRAM_MD5
+ }
+
private static final String STLS_COMMAND = "STLS";
private static final String USER_COMMAND = "USER";
private static final String PASS_COMMAND = "PASS";
@@ -52,11 +59,150 @@ public class Pop3Store extends Store {
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
+ /**
+ * Decodes a Pop3Store URI.
+ *
+ * Possible forms:
+ *
+ * pop3://user:password@server:port CONNECTION_SECURITY_NONE
+ * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+ * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+ * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+ * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ *
+ */
+ public static ServerSettings decodeUri(String uri) {
+ String host;
+ int port;
+ ConnectionSecurity connectionSecurity;
+ String username = null;
+ String password = null;
+
+ URI pop3Uri;
+ try {
+ pop3Uri = new URI(uri);
+ } catch (URISyntaxException use) {
+ throw new IllegalArgumentException("Invalid Pop3Store URI", use);
+ }
+
+ String scheme = pop3Uri.getScheme();
+ if (scheme.equals("pop3")) {
+ connectionSecurity = ConnectionSecurity.NONE;
+ port = 110;
+ } else if (scheme.equals("pop3+tls")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
+ port = 110;
+ } else if (scheme.equals("pop3+tls+")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
+ port = 110;
+ } else if (scheme.equals("pop3+ssl+")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
+ port = 995;
+ } else if (scheme.equals("pop3+ssl")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
+ port = 995;
+ } else {
+ throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
+ }
+
+ host = pop3Uri.getHost();
+
+ if (pop3Uri.getPort() != -1) {
+ port = pop3Uri.getPort();
+ }
+
+ String authType = AuthType.PLAIN.name();
+ if (pop3Uri.getUserInfo() != null) {
+ try {
+ int userIndex = 0, passwordIndex = 1;
+ String userinfo = pop3Uri.getUserInfo();
+ String[] userInfoParts = userinfo.split(":");
+ if (userInfoParts.length > 2 || userinfo.endsWith(":") ) {
+ // If 'userinfo' ends with ":" the password is empty. This can only happen
+ // after an account was imported (so authType and username are present).
+ userIndex++;
+ passwordIndex++;
+ authType = userInfoParts[0];
+ }
+ username = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
+ if (userInfoParts.length > passwordIndex) {
+ password = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
+ }
+ } catch (UnsupportedEncodingException enc) {
+ // This shouldn't happen since the encoding is hardcoded to UTF-8
+ throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
+ }
+ }
+
+ return new ServerSettings(STORE_TYPE, host, port, connectionSecurity, authType, username,
+ password);
+ }
+
+ /**
+ * Creates a Pop3Store URI with the supplied settings.
+ *
+ * @param server
+ * The {@link ServerSettings} object that holds the server settings.
+ *
+ * @return A Pop3Store URI that holds the same information as the {@code server} parameter.
+ *
+ * @see Account#getStoreUri()
+ * @see Pop3Store#decodeUri(String)
+ */
+ public static String createUri(ServerSettings server) {
+ String userEnc;
+ String passwordEnc;
+ try {
+ userEnc = URLEncoder.encode(server.username, "UTF-8");
+ passwordEnc = (server.password != null) ?
+ URLEncoder.encode(server.password, "UTF-8") : "";
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Could not encode username or password", e);
+ }
+
+ String scheme;
+ switch (server.connectionSecurity) {
+ case SSL_TLS_OPTIONAL:
+ scheme = "pop3+ssl";
+ break;
+ case SSL_TLS_REQUIRED:
+ scheme = "pop3+ssl+";
+ break;
+ case STARTTLS_OPTIONAL:
+ scheme = "pop3+tls";
+ break;
+ case STARTTLS_REQUIRED:
+ scheme = "pop3+tls+";
+ break;
+ default:
+ case NONE:
+ scheme = "pop3";
+ break;
+ }
+
+ try {
+ AuthType.valueOf(server.authenticationType);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Invalid authentication type (" +
+ server.authenticationType + ")");
+ }
+
+ String userInfo = server.authenticationType + ":" + userEnc + ":" + passwordEnc;
+ try {
+ return new URI(scheme, userInfo, server.host, server.port, null, null,
+ null).toString();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Can't create Pop3Store URI", e);
+ }
+ }
+
+
private String mHost;
private int mPort;
private String mUsername;
private String mPassword;
- private boolean useCramMd5;
+ private AuthType mAuthType;
private int mConnectionSecurity;
private HashMap mFolders = new HashMap();
private Pop3Capabilities mCapabilities;
@@ -68,68 +214,41 @@ public class Pop3Store extends Store {
*/
private boolean mTopNotSupported;
- /**
- * pop3://user:password@server:port CONNECTION_SECURITY_NONE
- * pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
- */
+
public Pop3Store(Account account) throws MessagingException {
super(account);
- URI uri;
+ ServerSettings settings;
try {
- uri = new URI(mAccount.getStoreUri());
- } catch (URISyntaxException use) {
- throw new MessagingException("Invalid Pop3Store URI", use);
+ settings = decodeUri(mAccount.getStoreUri());
+ } catch (IllegalArgumentException e) {
+ throw new MessagingException("Error while decoding store URI", e);
}
- String scheme = uri.getScheme();
- if (scheme.equals("pop3")) {
+ mHost = settings.host;
+ mPort = settings.port;
+
+ switch (settings.connectionSecurity) {
+ case NONE:
mConnectionSecurity = CONNECTION_SECURITY_NONE;
- mPort = 110;
- } else if (scheme.equals("pop3+tls")) {
+ break;
+ case STARTTLS_OPTIONAL:
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- mPort = 110;
- } else if (scheme.equals("pop3+tls+")) {
+ break;
+ case STARTTLS_REQUIRED:
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- mPort = 110;
- } else if (scheme.equals("pop3+ssl+")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- mPort = 995;
- } else if (scheme.equals("pop3+ssl")) {
+ break;
+ case SSL_TLS_OPTIONAL:
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- mPort = 995;
- } else {
- throw new MessagingException("Unsupported protocol");
+ break;
+ case SSL_TLS_REQUIRED:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
+ break;
}
- mHost = uri.getHost();
-
- if (uri.getPort() != -1) {
- mPort = uri.getPort();
- }
-
- useCramMd5 = false;
- if (uri.getUserInfo() != null) {
- try {
- int userIndex = 0, passwordIndex = 1;
- String[] userInfoParts = uri.getUserInfo().split(":");
- if (userInfoParts.length > 2) {
- userIndex++;
- passwordIndex++;
- useCramMd5 = true;
- }
- mUsername = URLDecoder.decode(userInfoParts[userIndex], "UTF-8");
- if (userInfoParts.length > passwordIndex) {
- mPassword = URLDecoder.decode(userInfoParts[passwordIndex], "UTF-8");
- }
- } catch (UnsupportedEncodingException enc) {
- // This shouldn't happen since the encoding is hardcoded to UTF-8
- Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc);
- }
- }
+ mUsername = settings.username;
+ mPassword = settings.password;
+ mAuthType = AuthType.valueOf(settings.authenticationType);
}
@Override
@@ -249,7 +368,7 @@ public class Pop3Store extends Store {
}
}
- if (useCramMd5) {
+ if (mAuthType == AuthType.CRAM_MD5) {
try {
String b64Nonce = executeSimpleCommand("AUTH CRAM-MD5").replace("+ ", "");
diff --git a/src/com/fsck/k9/mail/store/WebDavStore.java b/src/com/fsck/k9/mail/store/WebDavStore.java
index 8acc2ed02..17f3cc98b 100644
--- a/src/com/fsck/k9/mail/store/WebDavStore.java
+++ b/src/com/fsck/k9/mail/store/WebDavStore.java
@@ -56,6 +56,8 @@ import java.util.zip.GZIPInputStream;
*
*/
public class WebDavStore extends Store {
+ public static final String STORE_TYPE = "WebDAV";
+
// Security options
private static final short CONNECTION_SECURITY_NONE = 0;
private static final short CONNECTION_SECURITY_TLS_OPTIONAL = 1;
@@ -84,12 +86,228 @@ public class WebDavStore extends Store {
private static final String DAV_MAIL_OUTBOX_FOLDER = "outbox";
private static final String DAV_MAIL_SENT_FOLDER = "sentitems";
+
+ /**
+ * Decodes a WebDavStore URI.
+ *
+ * Possible forms:
+ *
+ * webdav://user:password@server:port CONNECTION_SECURITY_NONE
+ * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
+ * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
+ * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
+ * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
+ *
+ */
+ public static WebDavStoreSettings decodeUri(String uri) {
+ String host;
+ int port;
+ ConnectionSecurity connectionSecurity;
+ String username = null;
+ String password = null;
+ String alias = null;
+ String path = null;
+ String authPath = null;
+ String mailboxPath = null;
+
+
+ URI webDavUri;
+ try {
+ webDavUri = new URI(uri);
+ } catch (URISyntaxException use) {
+ throw new IllegalArgumentException("Invalid WebDavStore URI", use);
+ }
+
+ String scheme = webDavUri.getScheme();
+ if (scheme.equals("webdav")) {
+ connectionSecurity = ConnectionSecurity.NONE;
+ } else if (scheme.equals("webdav+ssl")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
+ } else if (scheme.equals("webdav+ssl+")) {
+ connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
+ } else if (scheme.equals("webdav+tls")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
+ } else if (scheme.equals("webdav+tls+")) {
+ connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
+ } else {
+ throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
+ }
+
+ host = webDavUri.getHost();
+ if (host.startsWith("http")) {
+ String[] hostParts = host.split("://", 2);
+ if (hostParts.length > 1) {
+ host = hostParts[1];
+ }
+ }
+
+ port = webDavUri.getPort();
+
+ String userInfo = webDavUri.getUserInfo();
+ if (userInfo != null) {
+ try {
+ String[] userInfoParts = userInfo.split(":");
+ username = URLDecoder.decode(userInfoParts[0], "UTF-8");
+ String userParts[] = username.split("\\\\", 2);
+
+ if (userParts.length > 1) {
+ alias = userParts[1];
+ } else {
+ alias = username;
+ }
+ if (userInfoParts.length > 1) {
+ password = URLDecoder.decode(userInfoParts[1], "UTF-8");
+ }
+ } catch (UnsupportedEncodingException enc) {
+ // This shouldn't happen since the encoding is hardcoded to UTF-8
+ throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
+ }
+ }
+
+ String[] pathParts = webDavUri.getPath().split("\\|");
+ for (int i = 0, count = pathParts.length; i < count; i++) {
+ if (i == 0) {
+ if (pathParts[0] != null &&
+ pathParts[0].length() > 1) {
+ path = pathParts[0];
+ }
+ } else if (i == 1) {
+ if (pathParts[1] != null &&
+ pathParts[1].length() > 1) {
+ authPath = pathParts[1];
+ }
+ } else if (i == 2) {
+ if (pathParts[2] != null &&
+ pathParts[2].length() > 1) {
+ mailboxPath = pathParts[2];
+ }
+ }
+ }
+
+ return new WebDavStoreSettings(host, port, connectionSecurity, null, username, password,
+ alias, path, authPath, mailboxPath);
+ }
+
+ /**
+ * Creates a WebDavStore URI with the supplied settings.
+ *
+ * @param server
+ * The {@link ServerSettings} object that holds the server settings.
+ *
+ * @return A WebDavStore URI that holds the same information as the {@code server} parameter.
+ *
+ * @see Account#getStoreUri()
+ * @see WebDavStore#decodeUri(String)
+ */
+ public static String createUri(ServerSettings server) {
+ String userEnc;
+ String passwordEnc;
+ try {
+ userEnc = URLEncoder.encode(server.username, "UTF-8");
+ passwordEnc = (server.password != null) ?
+ URLEncoder.encode(server.password, "UTF-8") : "";
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Could not encode username or password", e);
+ }
+
+ String scheme;
+ switch (server.connectionSecurity) {
+ case SSL_TLS_OPTIONAL:
+ scheme = "webdav+ssl";
+ break;
+ case SSL_TLS_REQUIRED:
+ scheme = "webdav+ssl+";
+ break;
+ case STARTTLS_OPTIONAL:
+ scheme = "webdav+tls";
+ break;
+ case STARTTLS_REQUIRED:
+ scheme = "webdav+tls+";
+ break;
+ default:
+ case NONE:
+ scheme = "webdav";
+ break;
+ }
+
+ String userInfo = userEnc + ":" + passwordEnc;
+
+ String uriPath;
+ Map extra = server.getExtra();
+ if (extra != null) {
+ String path = extra.get(WebDavStoreSettings.PATH_KEY);
+ path = (path != null) ? path : "";
+ String authPath = extra.get(WebDavStoreSettings.AUTH_PATH_KEY);
+ authPath = (authPath != null) ? authPath : "";
+ String mailboxPath = extra.get(WebDavStoreSettings.MAILBOX_PATH_KEY);
+ mailboxPath = (mailboxPath != null) ? mailboxPath : "";
+ uriPath = path + "|" + authPath + "|" + mailboxPath;
+ } else {
+ uriPath = "||";
+ }
+
+ try {
+ return new URI(scheme, userInfo, server.host, server.port, uriPath,
+ null, null).toString();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Can't create WebDavStore URI", e);
+ }
+ }
+
+
+ /**
+ * This class is used to store the decoded contents of an WebDavStore URI.
+ *
+ * @see WebDavStore#decodeUri(String)
+ */
+ private static class WebDavStoreSettings extends ServerSettings {
+ private static final String ALIAS_KEY = "alias";
+ private static final String PATH_KEY = "path";
+ private static final String AUTH_PATH_KEY = "authPath";
+ private static final String MAILBOX_PATH_KEY = "mailboxPath";
+
+ public final String alias;
+ public final String path;
+ public final String authPath;
+ public final String mailboxPath;
+
+ protected WebDavStoreSettings(String host, int port, ConnectionSecurity connectionSecurity,
+ String authenticationType, String username, String password, String alias,
+ String path, String authPath, String mailboxPath) {
+ super(STORE_TYPE, host, port, connectionSecurity, authenticationType, username,
+ password);
+ this.alias = alias;
+ this.path = path;
+ this.authPath = authPath;
+ this.mailboxPath = mailboxPath;
+ }
+
+ @Override
+ public Map getExtra() {
+ Map extra = new HashMap();
+ putIfNotNull(extra, ALIAS_KEY, alias);
+ putIfNotNull(extra, PATH_KEY, path);
+ putIfNotNull(extra, AUTH_PATH_KEY, authPath);
+ putIfNotNull(extra, MAILBOX_PATH_KEY, mailboxPath);
+ return extra;
+ }
+
+ @Override
+ public ServerSettings newPassword(String newPassword) {
+ return new WebDavStoreSettings(host, port, connectionSecurity, authenticationType,
+ username, newPassword, alias, path, authPath, mailboxPath);
+ }
+ }
+
+
private short mConnectionSecurity;
private String mUsername; /* Stores the username for authentications */
private String mAlias; /* Stores the alias for the user's mailbox */
private String mPassword; /* Stores the password for authentications */
private String mUrl; /* Stores the base URL for the server */
private String mHost; /* Stores the host name for the server */
+ private int mPort;
private String mPath; /* Stores the path for the server */
private String mAuthPath; /* Stores the path off of the server to post data to for form based authentication */
private String mMailboxPath; /* Stores the user specified path to the mailbox */
@@ -106,85 +324,46 @@ public class WebDavStore extends Store {
private Folder mSendFolder = null;
private HashMap mFolderList = new HashMap();
- /**
- * webdav://user:password@server:port CONNECTION_SECURITY_NONE
- * webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
- * webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
- * webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
- * webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
- */
+
public WebDavStore(Account account) throws MessagingException {
super(account);
+ WebDavStoreSettings settings;
try {
- mUri = new URI(mAccount.getStoreUri());
- } catch (URISyntaxException use) {
- throw new MessagingException("Invalid WebDavStore URI", use);
+ settings = decodeUri(mAccount.getStoreUri());
+ } catch (IllegalArgumentException e) {
+ throw new MessagingException("Error while decoding store URI", e);
}
- String scheme = mUri.getScheme();
- if (scheme.equals("webdav")) {
+ mHost = settings.host;
+ mPort = settings.port;
+
+ switch (settings.connectionSecurity) {
+ case NONE:
mConnectionSecurity = CONNECTION_SECURITY_NONE;
- } else if (scheme.equals("webdav+ssl")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- } else if (scheme.equals("webdav+ssl+")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- } else if (scheme.equals("webdav+tls")) {
+ break;
+ case STARTTLS_OPTIONAL:
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- } else if (scheme.equals("webdav+tls+")) {
+ break;
+ case STARTTLS_REQUIRED:
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- } else {
- throw new MessagingException("Unsupported protocol");
+ break;
+ case SSL_TLS_OPTIONAL:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
+ break;
+ case SSL_TLS_REQUIRED:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
+ break;
}
- mHost = mUri.getHost();
- if (mHost.startsWith("http")) {
- String[] hostParts = mHost.split("://", 2);
- if (hostParts.length > 1) {
- mHost = hostParts[1];
- }
- }
+ mUsername = settings.username;
+ mPassword = settings.password;
+ mAlias = settings.alias;
- if (mUri.getUserInfo() != null) {
- try {
- String[] userInfoParts = mUri.getUserInfo().split(":");
- mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8");
- String userParts[] = mUsername.split("\\\\", 2);
+ mPath = settings.path;
+ mAuthPath = settings.authPath;
+ mMailboxPath = settings.mailboxPath;
- if (userParts.length > 1) {
- mAlias = userParts[1];
- } else {
- mAlias = mUsername;
- }
- if (userInfoParts.length > 1) {
- mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8");
- }
- } catch (UnsupportedEncodingException enc) {
- // This shouldn't happen since the encoding is hardcoded to UTF-8
- Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc);
- }
- }
-
- String[] pathParts = mUri.getPath().split("\\|");
-
- for (int i = 0, count = pathParts.length; i < count; i++) {
- if (i == 0) {
- if (pathParts[0] != null &&
- pathParts[0].length() > 1) {
- mPath = pathParts[0];
- }
- } else if (i == 1) {
- if (pathParts[1] != null &&
- pathParts[1].length() > 1) {
- mAuthPath = pathParts[1];
- }
- } else if (i == 2) {
- if (pathParts[2] != null &&
- pathParts[2].length() > 1) {
- mMailboxPath = pathParts[2];
- }
- }
- }
if (mPath == null || mPath.equals("")) {
mPath = "/Exchange";
@@ -222,7 +401,7 @@ public class WebDavStore extends Store {
} else {
root = "http";
}
- root += "://" + mHost + ":" + mUri.getPort();
+ root += "://" + mHost + ":" + mPort;
return root;
}
diff --git a/src/com/fsck/k9/mail/transport/SmtpTransport.java b/src/com/fsck/k9/mail/transport/SmtpTransport.java
index e7546190a..4b3c135f1 100644
--- a/src/com/fsck/k9/mail/transport/SmtpTransport.java
+++ b/src/com/fsck/k9/mail/transport/SmtpTransport.java
@@ -2,6 +2,7 @@
package com.fsck.k9.mail.transport;
import android.util.Log;
+import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.mail.*;
import com.fsck.k9.mail.Message.RecipientType;
@@ -29,14 +30,12 @@ import java.security.SecureRandom;
import java.util.*;
public class SmtpTransport extends Transport {
+ public static final String TRANSPORT_TYPE = "SMTP";
+
public static final int CONNECTION_SECURITY_NONE = 0;
-
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
-
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
-
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
-
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
public static final String AUTH_PLAIN = "PLAIN";
@@ -47,87 +46,187 @@ public class SmtpTransport extends Transport {
public static final String AUTH_AUTOMATIC = "AUTOMATIC";
- String mHost;
-
- int mPort;
-
- String mUsername;
-
- String mPassword;
-
- String mAuthType;
-
- int mConnectionSecurity;
-
- boolean mSecure;
-
- Socket mSocket;
-
- PeekableInputStream mIn;
-
- OutputStream mOut;
- private boolean m8bitEncodingAllowed;
-
- private int mLargestAcceptableMessage;
/**
+ * Decodes a SmtpTransport URI.
+ *
+ * Possible forms:
+ *
* smtp://user:password@server:port CONNECTION_SECURITY_NONE
* smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
* smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
* smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
* smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
- *
- * @param _uri
+ *
*/
- public SmtpTransport(String _uri) throws MessagingException {
- URI uri;
+ public static ServerSettings decodeUri(String uri) {
+ String host;
+ int port;
+ ConnectionSecurity connectionSecurity;
+ String authenticationType = null;
+ String username = null;
+ String password = null;
+
+ URI smtpUri;
try {
- uri = new URI(_uri);
+ smtpUri = new URI(uri);
} catch (URISyntaxException use) {
- throw new MessagingException("Invalid SmtpTransport URI", use);
+ throw new IllegalArgumentException("Invalid SmtpTransport URI", use);
}
- String scheme = uri.getScheme();
+ String scheme = smtpUri.getScheme();
if (scheme.equals("smtp")) {
- mConnectionSecurity = CONNECTION_SECURITY_NONE;
- mPort = 25;
+ connectionSecurity = ConnectionSecurity.NONE;
+ port = 25;
} else if (scheme.equals("smtp+tls")) {
- mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
- mPort = 25;
+ connectionSecurity = ConnectionSecurity.STARTTLS_OPTIONAL;
+ port = 25;
} else if (scheme.equals("smtp+tls+")) {
- mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
- mPort = 25;
+ connectionSecurity = ConnectionSecurity.STARTTLS_REQUIRED;
+ port = 25;
} else if (scheme.equals("smtp+ssl+")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
- mPort = 465;
+ connectionSecurity = ConnectionSecurity.SSL_TLS_REQUIRED;
+ port = 465;
} else if (scheme.equals("smtp+ssl")) {
- mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
- mPort = 465;
+ connectionSecurity = ConnectionSecurity.SSL_TLS_OPTIONAL;
+ port = 465;
} else {
- throw new MessagingException("Unsupported protocol");
+ throw new IllegalArgumentException("Unsupported protocol (" + scheme + ")");
}
- mHost = uri.getHost();
+ host = smtpUri.getHost();
- if (uri.getPort() != -1) {
- mPort = uri.getPort();
+ if (smtpUri.getPort() != -1) {
+ port = smtpUri.getPort();
}
- if (uri.getUserInfo() != null) {
+ if (smtpUri.getUserInfo() != null) {
try {
- String[] userInfoParts = uri.getUserInfo().split(":");
- mUsername = URLDecoder.decode(userInfoParts[0], "UTF-8");
+ String[] userInfoParts = smtpUri.getUserInfo().split(":");
+ username = URLDecoder.decode(userInfoParts[0], "UTF-8");
if (userInfoParts.length > 1) {
- mPassword = URLDecoder.decode(userInfoParts[1], "UTF-8");
+ password = URLDecoder.decode(userInfoParts[1], "UTF-8");
}
if (userInfoParts.length > 2) {
- mAuthType = userInfoParts[2];
+ authenticationType = userInfoParts[2];
}
} catch (UnsupportedEncodingException enc) {
// This shouldn't happen since the encoding is hardcoded to UTF-8
- Log.e(K9.LOG_TAG, "Couldn't urldecode username or password.", enc);
+ throw new IllegalArgumentException("Couldn't urldecode username or password.", enc);
}
}
+
+ return new ServerSettings(TRANSPORT_TYPE, host, port, connectionSecurity,
+ authenticationType, username, password);
+ }
+
+ /**
+ * Creates a SmtpTransport URI with the supplied settings.
+ *
+ * @param server
+ * The {@link ServerSettings} object that holds the server settings.
+ *
+ * @return A SmtpTransport URI that holds the same information as the {@code server} parameter.
+ *
+ * @see Account#getTransportUri()
+ * @see SmtpTransport#decodeUri(String)
+ */
+ public static String createUri(ServerSettings server) {
+ String userEnc;
+ String passwordEnc;
+ try {
+ userEnc = (server.username != null) ?
+ URLEncoder.encode(server.username, "UTF-8") : "";
+ passwordEnc = (server.password != null) ?
+ URLEncoder.encode(server.password, "UTF-8") : "";
+ }
+ catch (UnsupportedEncodingException e) {
+ throw new IllegalArgumentException("Could not encode username or password", e);
+ }
+
+ String scheme;
+ switch (server.connectionSecurity) {
+ case SSL_TLS_OPTIONAL:
+ scheme = "smtp+ssl";
+ break;
+ case SSL_TLS_REQUIRED:
+ scheme = "smtp+ssl+";
+ break;
+ case STARTTLS_OPTIONAL:
+ scheme = "smtp+tls";
+ break;
+ case STARTTLS_REQUIRED:
+ scheme = "smtp+tls+";
+ break;
+ default:
+ case NONE:
+ scheme = "smtp";
+ break;
+ }
+
+ String authType = server.authenticationType;
+ if (!(AUTH_AUTOMATIC.equals(authType) ||
+ AUTH_LOGIN.equals(authType) ||
+ AUTH_PLAIN.equals(authType) ||
+ AUTH_CRAM_MD5.equals(authType))) {
+ throw new IllegalArgumentException("Invalid authentication type: " + authType);
+ }
+
+ String userInfo = userEnc + ":" + passwordEnc + ":" + authType;
+ try {
+ return new URI(scheme, userInfo, server.host, server.port, null, null,
+ null).toString();
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Can't create SmtpTransport URI", e);
+ }
+ }
+
+
+ String mHost;
+ int mPort;
+ String mUsername;
+ String mPassword;
+ String mAuthType;
+ int mConnectionSecurity;
+ boolean mSecure;
+ Socket mSocket;
+ PeekableInputStream mIn;
+ OutputStream mOut;
+ private boolean m8bitEncodingAllowed;
+ private int mLargestAcceptableMessage;
+
+ public SmtpTransport(String uri) throws MessagingException {
+ ServerSettings settings;
+ try {
+ settings = decodeUri(uri);
+ } catch (IllegalArgumentException e) {
+ throw new MessagingException("Error while decoding transport URI", e);
+ }
+
+ mHost = settings.host;
+ mPort = settings.port;
+
+ switch (settings.connectionSecurity) {
+ case NONE:
+ mConnectionSecurity = CONNECTION_SECURITY_NONE;
+ break;
+ case STARTTLS_OPTIONAL:
+ mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
+ break;
+ case STARTTLS_REQUIRED:
+ mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
+ break;
+ case SSL_TLS_OPTIONAL:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
+ break;
+ case SSL_TLS_REQUIRED:
+ mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
+ break;
+ }
+
+ mAuthType = settings.authenticationType;
+ mUsername = settings.username;
+ mPassword = settings.password;
}
@Override
diff --git a/src/com/fsck/k9/mail/transport/WebDavTransport.java b/src/com/fsck/k9/mail/transport/WebDavTransport.java
index 72159a5e6..6b5b61d60 100644
--- a/src/com/fsck/k9/mail/transport/WebDavTransport.java
+++ b/src/com/fsck/k9/mail/transport/WebDavTransport.java
@@ -7,10 +7,38 @@ import com.fsck.k9.Account;
import com.fsck.k9.K9;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Transport;
import com.fsck.k9.mail.store.WebDavStore;
public class WebDavTransport extends Transport {
+ public static final String TRANSPORT_TYPE = WebDavStore.STORE_TYPE;
+
+ /**
+ * Decodes a WebDavTransport URI.
+ *
+ *
+ * Note: Everything related to sending messages via WebDAV is handled by
+ * {@link WebDavStore}. So the transport URI is the same as the store URI.
+ *
+ */
+ public static ServerSettings decodeUri(String uri) {
+ return WebDavStore.decodeUri(uri);
+ }
+
+ /**
+ * Creates a WebDavTransport URI.
+ *
+ *
+ * Note: Everything related to sending messages via WebDAV is handled by
+ * {@link WebDavStore}. So the transport URI is the same as the store URI.
+ *
+ */
+ public static String createUri(ServerSettings server) {
+ return WebDavStore.createUri(server);
+ }
+
+
private WebDavStore store;
public WebDavTransport(Account account) throws MessagingException {
diff --git a/src/com/fsck/k9/preferences/AccountSettings.java b/src/com/fsck/k9/preferences/AccountSettings.java
new file mode 100644
index 000000000..13c4d089a
--- /dev/null
+++ b/src/com/fsck/k9/preferences/AccountSettings.java
@@ -0,0 +1,254 @@
+package com.fsck.k9.preferences;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import android.content.SharedPreferences;
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.R;
+import com.fsck.k9.Account.FolderMode;
+import com.fsck.k9.Account.ScrollButtons;
+import com.fsck.k9.crypto.Apg;
+import com.fsck.k9.mail.store.StorageManager;
+import com.fsck.k9.preferences.Settings.*;
+
+public class AccountSettings {
+ public static final Map SETTINGS;
+
+ static {
+ Map s = new LinkedHashMap();
+
+ s.put("archiveFolderName", new StringSetting("Archive"));
+ s.put("autoExpandFolderName", new StringSetting("INBOX"));
+ s.put("automaticCheckIntervalMinutes",
+ new IntegerResourceSetting(-1, R.array.account_settings_check_frequency_values));
+ s.put("chipColor", new ColorSetting(0xFF0000FF));
+ s.put("cryptoApp", new StringSetting(Apg.NAME));
+ s.put("cryptoAutoSignature", new BooleanSetting(false));
+ s.put("defaultQuotedTextShown", new BooleanSetting(Account.DEFAULT_QUOTED_TEXT_SHOWN));
+ s.put("deletePolicy", new DeletePolicySetting(Account.DELETE_POLICY_NEVER));
+ s.put("displayCount", new IntegerResourceSetting(K9.DEFAULT_VISIBLE_LIMIT,
+ R.array.account_settings_display_count_values));
+ s.put("draftsFolderName", new StringSetting("Drafts"));
+ s.put("enableMoveButtons", new BooleanSetting(false));
+ s.put("expungePolicy", new StringResourceSetting(Account.EXPUNGE_IMMEDIATELY,
+ R.array.account_setup_expunge_policy_values));
+ s.put("folderDisplayMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS));
+ s.put("folderPushMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS));
+ s.put("folderSyncMode", new EnumSetting(FolderMode.class, FolderMode.FIRST_CLASS));
+ s.put("folderTargetMode", new EnumSetting(FolderMode.class, FolderMode.NOT_SECOND_CLASS));
+ s.put("goToUnreadMessageSearch", new BooleanSetting(false));
+ s.put("hideButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER));
+ s.put("hideMoveButtonsEnum", new EnumSetting(ScrollButtons.class, ScrollButtons.NEVER));
+ s.put("idleRefreshMinutes", new IntegerResourceSetting(24,
+ R.array.idle_refresh_period_values));
+ s.put("inboxFolderName", new StringSetting("INBOX"));
+ s.put("led", new BooleanSetting(true));
+ s.put("ledColor", new ColorSetting(0xFF0000FF));
+ s.put("localStorageProvider", new StorageProviderSetting());
+ s.put("maxPushFolders", new IntegerRangeSetting(0, 100, 10));
+ s.put("maximumAutoDownloadMessageSize", new IntegerResourceSetting(32768,
+ R.array.account_settings_autodownload_message_size_values));
+ s.put("maximumPolledMessageAge", new IntegerResourceSetting(-1,
+ R.array.account_settings_message_age_values));
+ s.put("messageFormat",
+ new EnumSetting(Account.MessageFormat.class, Account.DEFAULT_MESSAGE_FORMAT));
+ s.put("messageReadReceipt", new BooleanSetting(Account.DEFAULT_MESSAGE_READ_RECEIPT));
+ s.put("notificationUnreadCount", new BooleanSetting(true));
+ s.put("notifyMailCheck", new BooleanSetting(false));
+ s.put("notifyNewMail", new BooleanSetting(false));
+ s.put("notifySelfNewMail", new BooleanSetting(true));
+ s.put("pushPollOnConnect", new BooleanSetting(true));
+ s.put("quotePrefix", new StringSetting(Account.DEFAULT_QUOTE_PREFIX));
+ s.put("quoteStyle",
+ new EnumSetting(Account.QuoteStyle.class, Account.DEFAULT_QUOTE_STYLE));
+ s.put("replyAfterQuote", new BooleanSetting(Account.DEFAULT_REPLY_AFTER_QUOTE));
+ s.put("ring", new BooleanSetting(true));
+ s.put("ringtone", new RingtoneSetting("content://settings/system/notification_sound"));
+ s.put("saveAllHeaders", new BooleanSetting(true));
+ s.put("searchableFolders",
+ new EnumSetting(Account.Searchable.class, Account.Searchable.ALL));
+ s.put("sentFolderName", new StringSetting("Sent"));
+ s.put("showPicturesEnum",
+ new EnumSetting(Account.ShowPictures.class, Account.ShowPictures.NEVER));
+ s.put("signatureBeforeQuotedText", new BooleanSetting(false));
+ s.put("spamFolderName", new StringSetting("Spam"));
+ s.put("subscribedFoldersOnly", new BooleanSetting(false));
+ s.put("syncRemoteDeletions", new BooleanSetting(true));
+ s.put("trashFolderName", new StringSetting("Trash"));
+ s.put("useCompression.MOBILE", new BooleanSetting(true));
+ s.put("useCompression.OTHER", new BooleanSetting(true));
+ s.put("useCompression.WIFI", new BooleanSetting(true));
+ s.put("vibrate", new BooleanSetting(false));
+ s.put("vibratePattern", new IntegerResourceSetting(0,
+ R.array.account_settings_vibrate_pattern_values));
+ s.put("vibrateTimes", new IntegerResourceSetting(5,
+ R.array.account_settings_vibrate_times_label));
+
+ SETTINGS = Collections.unmodifiableMap(s);
+ }
+
+ public static Map validate(Map importedSettings,
+ boolean useDefaultValues) {
+ return Settings.validate(SETTINGS, importedSettings, useDefaultValues);
+ }
+
+ public static Map getAccountSettings(SharedPreferences storage, String uuid) {
+ Map result = new HashMap();
+ String prefix = uuid + ".";
+ for (String key : SETTINGS.keySet()) {
+ String value = storage.getString(prefix + key, null);
+ if (value != null) {
+ result.put(key, value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * An integer resource setting.
+ *
+ *
+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array containing
+ * integer strings.
+ *
+ */
+ public static class IntegerResourceSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public IntegerResourceSetting(int defaultValue, int resId) {
+ super(defaultValue);
+
+ Map mapping = new HashMap();
+ String[] values = K9.app.getResources().getStringArray(resId);
+ for (String value : values) {
+ int intValue = Integer.parseInt(value);
+ mapping.put(intValue, value);
+ }
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new InvalidSettingValueException();
+ }
+ }
+ }
+
+ /**
+ * A string resource setting.
+ *
+ *
+ * Basically a {@link PseudoEnumSetting} that is initialized from a resource array.
+ *
+ */
+ public static class StringResourceSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public StringResourceSetting(String defaultValue, int resId) {
+ super(defaultValue);
+
+ Map mapping = new HashMap();
+ String[] values = K9.app.getResources().getStringArray(resId);
+ for (String value : values) {
+ mapping.put(value, value);
+ }
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ if (!mMapping.containsKey(value)) {
+ throw new InvalidSettingValueException();
+ }
+ return value;
+ }
+ }
+
+ /**
+ * The notification ringtone setting.
+ */
+ public static class RingtoneSetting extends SettingsDescription {
+ public RingtoneSetting(String defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) {
+ //TODO: add validation
+ return value;
+ }
+ }
+
+ /**
+ * The storage provider setting.
+ */
+ public static class StorageProviderSetting extends SettingsDescription {
+ public StorageProviderSetting() {
+ super(null);
+ }
+
+ @Override
+ public Object getDefaultValue() {
+ return StorageManager.getInstance(K9.app).getDefaultProviderId();
+ }
+
+ @Override
+ public Object fromString(String value) {
+ StorageManager storageManager = StorageManager.getInstance(K9.app);
+ Map providers = storageManager.getAvailableProviders();
+ if (providers.containsKey(value)) {
+ return value;
+ }
+ throw new RuntimeException("Validation failed");
+ }
+ }
+
+ /**
+ * The delete policy setting.
+ */
+ public static class DeletePolicySetting extends PseudoEnumSetting {
+ private Map mMapping;
+
+ public DeletePolicySetting(int defaultValue) {
+ super(defaultValue);
+ Map mapping = new HashMap();
+ mapping.put(Account.DELETE_POLICY_NEVER, "NEVER");
+ mapping.put(Account.DELETE_POLICY_ON_DELETE, "DELETE");
+ mapping.put(Account.DELETE_POLICY_MARK_AS_READ, "MARK_AS_READ");
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ Integer deletePolicy = Integer.parseInt(value);
+ if (mMapping.containsKey(deletePolicy)) {
+ return deletePolicy;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/FolderSettings.java b/src/com/fsck/k9/preferences/FolderSettings.java
new file mode 100644
index 000000000..1a2af5502
--- /dev/null
+++ b/src/com/fsck/k9/preferences/FolderSettings.java
@@ -0,0 +1,44 @@
+package com.fsck.k9.preferences;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import android.content.SharedPreferences;
+
+import com.fsck.k9.mail.Folder.FolderClass;
+import com.fsck.k9.preferences.Settings.*;
+
+public class FolderSettings {
+ public static final Map SETTINGS;
+
+ static {
+ Map s = new LinkedHashMap();
+
+ s.put("displayMode", new EnumSetting(FolderClass.class, FolderClass.NO_CLASS));
+ s.put("syncMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED));
+ s.put("pushMode", new EnumSetting(FolderClass.class, FolderClass.INHERITED));
+ s.put("inTopGroup", new BooleanSetting(false));
+ s.put("integrate", new BooleanSetting(false));
+
+ SETTINGS = Collections.unmodifiableMap(s);
+ }
+
+ public static Map validate(Map importedSettings,
+ boolean useDefaultValues) {
+ return Settings.validate(SETTINGS, importedSettings, useDefaultValues);
+ }
+
+ public static Map getFolderSettings(SharedPreferences storage, String uuid,
+ String folderName) {
+ Map result = new HashMap();
+ String prefix = uuid + "." + folderName + ".";
+ for (String key : SETTINGS.keySet()) {
+ String value = storage.getString(prefix + key, null);
+ if (value != null) {
+ result.put(key, value);
+ }
+ }
+ return result;
+ }
+}
diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java
new file mode 100644
index 000000000..746d96dbd
--- /dev/null
+++ b/src/com/fsck/k9/preferences/GlobalSettings.java
@@ -0,0 +1,256 @@
+package com.fsck.k9.preferences;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import android.content.SharedPreferences;
+import android.os.Environment;
+
+import com.fsck.k9.FontSizes;
+import com.fsck.k9.K9;
+import com.fsck.k9.R;
+import com.fsck.k9.helper.DateFormatter;
+import com.fsck.k9.preferences.Settings.*;
+
+public class GlobalSettings {
+ public static final Map SETTINGS;
+
+ static {
+ Map s = new LinkedHashMap();
+
+ s.put("animations", new BooleanSetting(false));
+ s.put("attachmentdefaultpath",
+ new DirectorySetting(Environment.getExternalStorageDirectory().toString()));
+ s.put("backgroundOperations",
+ new EnumSetting(K9.BACKGROUND_OPS.class, K9.BACKGROUND_OPS.WHEN_CHECKED));
+ s.put("changeRegisteredNameColor", new BooleanSetting(false));
+ s.put("compactLayouts", new BooleanSetting(false));
+ s.put("confirmDelete", new BooleanSetting(false));
+ s.put("confirmMarkAllAsRead", new BooleanSetting(false));
+ s.put("confirmSpam", new BooleanSetting(false));
+ s.put("countSearchMessages", new BooleanSetting(false));
+ s.put("dateFormat", new DateFormatSetting(DateFormatter.DEFAULT_FORMAT));
+ s.put("enableDebugLogging", new BooleanSetting(false));
+ s.put("enableSensitiveLogging", new BooleanSetting(false));
+ s.put("fontSizeAccountDescription", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeAccountName", new FontSizeSetting(FontSizes.MEDIUM));
+ s.put("fontSizeFolderName", new FontSizeSetting(FontSizes.LARGE));
+ s.put("fontSizeFolderStatus", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeMessageListDate", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeMessageListPreview", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeMessageListSender", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeMessageListSubject", new FontSizeSetting(FontSizes.FONT_16DIP));
+ s.put("fontSizeMessageViewAdditionalHeaders", new FontSizeSetting(FontSizes.FONT_12DIP));
+ s.put("fontSizeMessageViewCC", new FontSizeSetting(FontSizes.FONT_12DIP));
+ s.put("fontSizeMessageViewContent", new WebFontSizeSetting(3));
+ s.put("fontSizeMessageViewDate", new FontSizeSetting(FontSizes.FONT_10DIP));
+ s.put("fontSizeMessageViewSender", new FontSizeSetting(FontSizes.SMALL));
+ s.put("fontSizeMessageViewSubject", new FontSizeSetting(FontSizes.FONT_12DIP));
+ s.put("fontSizeMessageViewTime", new FontSizeSetting(FontSizes.FONT_10DIP));
+ s.put("fontSizeMessageViewTo", new FontSizeSetting(FontSizes.FONT_12DIP));
+ s.put("gesturesEnabled", new BooleanSetting(true));
+ s.put("hideSpecialAccounts", new BooleanSetting(false));
+ s.put("keyguardPrivacy", new BooleanSetting(false));
+ s.put("language", new LanguageSetting());
+ s.put("manageBack", new BooleanSetting(false));
+ s.put("measureAccounts", new BooleanSetting(true));
+ s.put("messageListCheckboxes", new BooleanSetting(false));
+ s.put("messageListPreviewLines", new IntegerRangeSetting(1, 100, 2));
+ s.put("messageListStars", new BooleanSetting(true));
+ s.put("messageListTouchable", new BooleanSetting(false));
+ s.put("messageViewFixedWidthFont", new BooleanSetting(false));
+ s.put("messageViewReturnToList", new BooleanSetting(false));
+ s.put("messageViewShowNext", new BooleanSetting(false));
+ s.put("mobileOptimizedLayout", new BooleanSetting(false));
+ s.put("quietTimeEnabled", new BooleanSetting(false));
+ s.put("quietTimeEnds", new TimeSetting("7:00"));
+ s.put("quietTimeStarts", new TimeSetting("21:00"));
+ s.put("registeredNameColor", new ColorSetting(0xFF00008F));
+ s.put("showContactName", new BooleanSetting(false));
+ s.put("showCorrespondentNames", new BooleanSetting(true));
+ s.put("startIntegratedInbox", new BooleanSetting(false));
+ s.put("theme", new ThemeSetting(android.R.style.Theme_Light));
+ s.put("useGalleryBugWorkaround", new GalleryBugWorkaroundSetting());
+ s.put("useVolumeKeysForListNavigation", new BooleanSetting(false));
+ s.put("useVolumeKeysForNavigation", new BooleanSetting(false));
+ s.put("zoomControlsEnabled", new BooleanSetting(false));
+
+ SETTINGS = Collections.unmodifiableMap(s);
+ }
+
+ public static Map validate(Map importedSettings) {
+ return Settings.validate(SETTINGS, importedSettings, false);
+ }
+
+ public static Map getGlobalSettings(SharedPreferences storage) {
+ Map result = new HashMap();
+ for (String key : SETTINGS.keySet()) {
+ String value = storage.getString(key, null);
+ if (value != null) {
+ result.put(key, value);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * The gallery bug work-around setting.
+ *
+ *
+ * The default value varies depending on whether you have a version of Gallery 3D installed
+ * that contains the bug we work around.
+ *
+ *
+ * @see K9#isGalleryBuggy()
+ */
+ public static class GalleryBugWorkaroundSetting extends BooleanSetting {
+ public GalleryBugWorkaroundSetting() {
+ super(false);
+ }
+
+ @Override
+ public Object getDefaultValue() {
+ return K9.isGalleryBuggy();
+ }
+ }
+
+ /**
+ * The language setting.
+ *
+ *
+ * Valid values are read from {@code settings_language_values} in
+ * {@code res/values/arrays.xml}.
+ *
+ */
+ public static class LanguageSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public LanguageSetting() {
+ super("");
+
+ Map mapping = new HashMap();
+ String[] values = K9.app.getResources().getStringArray(R.array.settings_language_values);
+ for (String value : values) {
+ if (value.length() == 0) {
+ mapping.put("", "default");
+ } else {
+ mapping.put(value, value);
+ }
+ }
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ if (mMapping.containsKey(value)) {
+ return value;
+ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * The theme setting.
+ */
+ public static class ThemeSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public ThemeSetting(int defaultValue) {
+ super(defaultValue);
+
+ Map mapping = new HashMap();
+ mapping.put(android.R.style.Theme_Light, "light");
+ mapping.put(android.R.style.Theme, "dark");
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ Integer theme = Integer.parseInt(value);
+ if (mMapping.containsKey(theme)) {
+ return theme;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * A date format setting.
+ */
+ public static class DateFormatSetting extends SettingsDescription {
+ public DateFormatSetting(String defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ // The placeholders "SHORT" and "MEDIUM" are fine.
+ if (DateFormatter.SHORT_FORMAT.equals(value) ||
+ DateFormatter.MEDIUM_FORMAT.equals(value)) {
+ return value;
+ }
+
+ // If the SimpleDateFormat constructor doesn't throw an exception, we're good.
+ new SimpleDateFormat(value);
+ return value;
+ } catch (Exception e) {
+ throw new InvalidSettingValueException();
+ }
+ }
+ }
+
+ /**
+ * A time setting.
+ */
+ public static class TimeSetting extends SettingsDescription {
+ public TimeSetting(String defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ if (!value.matches(TimePickerPreference.VALIDATION_EXPRESSION)) {
+ throw new InvalidSettingValueException();
+ }
+ return value;
+ }
+ }
+
+ /**
+ * A directory on the file system.
+ */
+ public static class DirectorySetting extends SettingsDescription {
+ public DirectorySetting(String defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ if (new File(value).isDirectory()) {
+ return value;
+ }
+ } catch (Exception e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/IdentitySettings.java b/src/com/fsck/k9/preferences/IdentitySettings.java
new file mode 100644
index 000000000..f796446e1
--- /dev/null
+++ b/src/com/fsck/k9/preferences/IdentitySettings.java
@@ -0,0 +1,99 @@
+package com.fsck.k9.preferences;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import android.content.SharedPreferences;
+
+import com.fsck.k9.EmailAddressValidator;
+import com.fsck.k9.K9;
+import com.fsck.k9.R;
+import com.fsck.k9.preferences.Settings.*;
+
+public class IdentitySettings {
+ public static final Map SETTINGS;
+
+ static {
+ Map s = new LinkedHashMap();
+
+ s.put("signature", new SignatureSetting());
+ s.put("signatureUse", new BooleanSetting(true));
+ s.put("replyTo", new OptionalEmailAddressSetting());
+
+ SETTINGS = Collections.unmodifiableMap(s);
+ }
+
+ public static Map validate(Map importedSettings,
+ boolean useDefaultValues) {
+ return Settings.validate(SETTINGS, importedSettings, useDefaultValues);
+ }
+
+ public static Map getIdentitySettings(SharedPreferences storage, String uuid,
+ int identityIndex) {
+ Map result = new HashMap();
+ String prefix = uuid + ".";
+ String suffix = "." + Integer.toString(identityIndex);
+ for (String key : SETTINGS.keySet()) {
+ String value = storage.getString(prefix + key + suffix, null);
+ if (value != null) {
+ result.put(key, value);
+ }
+ }
+ return result;
+ }
+
+
+ public static boolean isEmailAddressValid(String email) {
+ return new EmailAddressValidator().isValidAddressOnly(email);
+ }
+
+ /**
+ * The message signature setting.
+ */
+ public static class SignatureSetting extends SettingsDescription {
+ public SignatureSetting() {
+ super(null);
+ }
+
+ @Override
+ public Object getDefaultValue() {
+ return K9.app.getResources().getString(R.string.default_signature);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ return value;
+ }
+ }
+
+ /**
+ * An optional email address setting.
+ */
+ public static class OptionalEmailAddressSetting extends SettingsDescription {
+ private EmailAddressValidator mValidator;
+
+ public OptionalEmailAddressSetting() {
+ super(null);
+ mValidator = new EmailAddressValidator();
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ if (value != null && !mValidator.isValidAddressOnly(value)) {
+ throw new InvalidSettingValueException();
+ }
+ return value;
+ }
+
+ @Override
+ public String toPrettyString(Object value) {
+ return (value == null) ? "" : value.toString();
+ }
+
+ @Override
+ public Object fromPrettyString(String value) throws InvalidSettingValueException {
+ return ("".equals(value)) ? null : fromString(value);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java
new file mode 100644
index 000000000..8b2b00ace
--- /dev/null
+++ b/src/com/fsck/k9/preferences/Settings.java
@@ -0,0 +1,409 @@
+package com.fsck.k9.preferences;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import android.util.Log;
+
+import com.fsck.k9.FontSizes;
+import com.fsck.k9.K9;
+
+/*
+ * TODO:
+ * - add support for different settings versions (validate old version and upgrade to new format)
+ * - use the default values defined in GlobalSettings and AccountSettings when creating new
+ * accounts
+ * - think of a better way to validate enums than to use the resource arrays (i.e. get rid of
+ * ResourceArrayValidator); maybe even use the settings description for the settings UI
+ * - add unit test that validates the default values are actually valid according to the validator
+ */
+
+public class Settings {
+ /**
+ * Version number of global and account settings.
+ *
+ *
+ * This value is used as "version" attribute in the export file. It needs to be incremented
+ * when a global or account setting is added or removed, or when the format of a setting
+ * is changed (e.g. add a value to an enum).
+ *
+ *
+ * @see SettingsExporter
+ */
+ public static final int VERSION = 1;
+
+ public static Map validate(Map settings,
+ Map importedSettings, boolean useDefaultValues) {
+
+ Map validatedSettings = new HashMap();
+ for (Map.Entry setting : settings.entrySet()) {
+ String key = setting.getKey();
+ SettingsDescription desc = setting.getValue();
+
+ boolean useDefaultValue;
+ if (!importedSettings.containsKey(key)) {
+ Log.v(K9.LOG_TAG, "Key \"" + key + "\" wasn't found in the imported file." +
+ ((useDefaultValues) ? " Using default value." : ""));
+ useDefaultValue = useDefaultValues;
+ } else {
+ String prettyValue = importedSettings.get(key);
+ try {
+ Object internalValue = desc.fromPrettyString(prettyValue);
+ String importedValue = desc.toString(internalValue);
+ validatedSettings.put(key, importedValue);
+ useDefaultValue = false;
+ } catch (InvalidSettingValueException e) {
+ Log.v(K9.LOG_TAG, "Key \"" + key + "\" has invalid value \"" + prettyValue +
+ "\" in imported file. " +
+ ((useDefaultValues) ? "Using default value." : "Skipping."));
+ useDefaultValue = useDefaultValues;
+ }
+ }
+
+ if (useDefaultValue) {
+ Object defaultValue = desc.getDefaultValue();
+ String value = (defaultValue != null) ? desc.toString(defaultValue) : null;
+ validatedSettings.put(key, value);
+ }
+ }
+
+ return validatedSettings;
+ }
+
+
+ /**
+ * Indicates an invalid setting value.
+ *
+ * @see SettingsDescription#fromString(String)
+ * @see SettingsDescription#fromPrettyString(String)
+ */
+ public static class InvalidSettingValueException extends Exception {
+ private static final long serialVersionUID = 1L;
+ }
+
+ /**
+ * Describes a setting.
+ *
+ *
+ * Instances of this class are used to convert the string representations of setting values to
+ * an internal representation (e.g. an integer) and back.
+ *
+ * Currently we use two different string representations:
+ *
+ *
+ * -
+ * The one that is used by the internal preference {@link Storage}. It is usually obtained by
+ * calling {@code toString()} on the internal representation of the setting value (see e.g.
+ * {@link K9#save(android.content.SharedPreferences.Editor)}).
+ *
+ * -
+ * The "pretty" version that is used by the import/export settings file (e.g. colors are
+ * exported in #rrggbb format instead of a integer string like "-8734021").
+ *
+ *
+ *
+ * Note:
+ * For the future we should aim to get rid of the "internal" string representation. The
+ * "pretty" version makes reading a database dump easier and the performance impact should be
+ * negligible.
+ *
+ */
+ public static abstract class SettingsDescription {
+ /**
+ * The setting's default value (internal representation).
+ */
+ protected Object mDefaultValue;
+
+ public SettingsDescription(Object defaultValue) {
+ mDefaultValue = defaultValue;
+ }
+
+ /**
+ * Get the default value.
+ *
+ * @return The internal representation of the default value.
+ */
+ public Object getDefaultValue() {
+ return mDefaultValue;
+ }
+
+ /**
+ * Convert a setting's value to the string representation.
+ *
+ * @param value
+ * The internal representation of a setting's value.
+ *
+ * @return The string representation of {@code value}.
+ */
+ public String toString(Object value) {
+ return value.toString();
+ }
+
+ /**
+ * Parse the string representation of a setting's value .
+ *
+ * @param value
+ * The string representation of a setting's value.
+ *
+ * @return The internal representation of the setting's value.
+ *
+ * @throws InvalidSettingValueException
+ * If {@code value} contains an invalid value.
+ */
+ public abstract Object fromString(String value) throws InvalidSettingValueException;
+
+ /**
+ * Convert a setting value to the "pretty" string representation.
+ *
+ * @param value
+ * The setting's value.
+ *
+ * @return A pretty-printed version of the setting's value.
+ */
+ public String toPrettyString(Object value) {
+ return toString(value);
+ }
+
+ /**
+ * Convert the pretty-printed version of a setting's value to the internal representation.
+ *
+ * @param value
+ * The pretty-printed version of the setting's value. See
+ * {@link #toPrettyString(Object)}.
+ *
+ * @return The internal representation of the setting's value.
+ *
+ * @throws InvalidSettingValueException
+ * If {@code value} contains an invalid value.
+ */
+ public Object fromPrettyString(String value) throws InvalidSettingValueException {
+ return fromString(value);
+ }
+ }
+
+ /**
+ * A string setting.
+ */
+ public static class StringSetting extends SettingsDescription {
+ public StringSetting(String defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) {
+ return value;
+ }
+ }
+
+ /**
+ * A boolean setting.
+ */
+ public static class BooleanSetting extends SettingsDescription {
+ public BooleanSetting(boolean defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ if (Boolean.TRUE.toString().equals(value)) {
+ return true;
+ } else if (Boolean.FALSE.toString().equals(value)) {
+ return false;
+ }
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * A color setting.
+ */
+ public static class ColorSetting extends SettingsDescription {
+ public ColorSetting(int defaultValue) {
+ super(defaultValue);
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ return Integer.parseInt(value);
+ } catch (NumberFormatException e) {
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ @Override
+ public String toPrettyString(Object value) {
+ int color = ((Integer) value) & 0x00FFFFFF;
+ return String.format("#%06x", color);
+ }
+
+ @Override
+ public Object fromPrettyString(String value) throws InvalidSettingValueException {
+ try {
+ if (value.length() == 7) {
+ return Integer.parseInt(value.substring(1), 16) | 0xFF000000;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * An {@code Enum} setting.
+ *
+ *
+ * {@link Enum#toString()} is used to obtain the "pretty" string representation.
+ *
+ */
+ public static class EnumSetting extends SettingsDescription {
+ private Class extends Enum>> mEnumClass;
+
+ public EnumSetting(Class extends Enum>> enumClass, Object defaultValue) {
+ super(defaultValue);
+ mEnumClass = enumClass;
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ return Enum.valueOf((Class extends Enum>)mEnumClass, value);
+ } catch (Exception e) {
+ throw new InvalidSettingValueException();
+ }
+ }
+ }
+
+ /**
+ * A setting that has multiple valid values but doesn't use an {@link Enum} internally.
+ *
+ * @param
+ * The type of the internal representation (e.g. {@code Integer}).
+ */
+ public abstract static class PseudoEnumSetting extends SettingsDescription {
+ public PseudoEnumSetting(Object defaultValue) {
+ super(defaultValue);
+ }
+
+ protected abstract Map getMapping();
+
+ @Override
+ public String toPrettyString(Object value) {
+ return getMapping().get(value);
+ }
+
+ @Override
+ public Object fromPrettyString(String value) throws InvalidSettingValueException {
+ for (Entry entry : getMapping().entrySet()) {
+ if (entry.getValue().equals(value)) {
+ return entry.getKey();
+ }
+ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * A font size setting.
+ */
+ public static class FontSizeSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public FontSizeSetting(int defaultValue) {
+ super(defaultValue);
+
+ Map mapping = new HashMap();
+ mapping.put(FontSizes.FONT_10DIP, "tiniest");
+ mapping.put(FontSizes.FONT_12DIP, "tiny");
+ mapping.put(FontSizes.SMALL, "smaller");
+ mapping.put(FontSizes.FONT_16DIP, "small");
+ mapping.put(FontSizes.MEDIUM, "medium");
+ mapping.put(FontSizes.FONT_20DIP, "large");
+ mapping.put(FontSizes.LARGE, "larger");
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ Integer fontSize = Integer.parseInt(value);
+ if (mMapping.containsKey(fontSize)) {
+ return fontSize;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * A {@link android.webkit.WebView} font size setting.
+ */
+ public static class WebFontSizeSetting extends PseudoEnumSetting {
+ private final Map mMapping;
+
+ public WebFontSizeSetting(int defaultValue) {
+ super(defaultValue);
+
+ Map mapping = new HashMap();
+ mapping.put(1, "smallest");
+ mapping.put(2, "smaller");
+ mapping.put(3, "normal");
+ mapping.put(4, "larger");
+ mapping.put(5, "largest");
+ mMapping = Collections.unmodifiableMap(mapping);
+ }
+
+ @Override
+ protected Map getMapping() {
+ return mMapping;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ Integer fontSize = Integer.parseInt(value);
+ if (mMapping.containsKey(fontSize)) {
+ return fontSize;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+
+ /**
+ * An integer settings whose values a limited to a certain range.
+ */
+ public static class IntegerRangeSetting extends SettingsDescription {
+ private int mStart;
+ private int mEnd;
+
+ public IntegerRangeSetting(int start, int end, int defaultValue) {
+ super(defaultValue);
+ mStart = start;
+ mEnd = end;
+ }
+
+ @Override
+ public Object fromString(String value) throws InvalidSettingValueException {
+ try {
+ int intValue = Integer.parseInt(value);
+ if (mStart <= intValue && intValue <= mEnd) {
+ return intValue;
+ }
+ } catch (NumberFormatException e) { /* do nothing */ }
+
+ throw new InvalidSettingValueException();
+ }
+ }
+}
diff --git a/src/com/fsck/k9/preferences/SettingsExporter.java b/src/com/fsck/k9/preferences/SettingsExporter.java
new file mode 100644
index 000000000..cabc8cdf2
--- /dev/null
+++ b/src/com/fsck/k9/preferences/SettingsExporter.java
@@ -0,0 +1,483 @@
+package com.fsck.k9.preferences;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.Map.Entry;
+import org.xmlpull.v1.XmlSerializer;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Environment;
+import android.util.Log;
+import android.util.Xml;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.Store;
+import com.fsck.k9.mail.ServerSettings;
+import com.fsck.k9.mail.Transport;
+import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
+import com.fsck.k9.preferences.Settings.SettingsDescription;
+
+
+public class SettingsExporter {
+ private static final String EXPORT_FILENAME = "settings.k9s";
+
+ /**
+ * File format version number.
+ *
+ *
+ * Increment this if you need to change the structure of the settings file. When you do this
+ * remember that we also have to be able to handle old file formats. So have fun adding support
+ * for that to {@link SettingsImporter} :)
+ *
+ */
+ public static final int FILE_FORMAT_VERSION = 1;
+
+ public static final String ROOT_ELEMENT = "k9settings";
+ public static final String VERSION_ATTRIBUTE = "version";
+ public static final String FILE_FORMAT_ATTRIBUTE = "format";
+ public static final String GLOBAL_ELEMENT = "global";
+ public static final String SETTINGS_ELEMENT = "settings";
+ public static final String ACCOUNTS_ELEMENT = "accounts";
+ public static final String ACCOUNT_ELEMENT = "account";
+ public static final String UUID_ATTRIBUTE = "uuid";
+ public static final String INCOMING_SERVER_ELEMENT = "incoming-server";
+ public static final String OUTGOING_SERVER_ELEMENT = "outgoing-server";
+ public static final String TYPE_ATTRIBUTE = "type";
+ public static final String HOST_ELEMENT = "host";
+ public static final String PORT_ELEMENT = "port";
+ public static final String CONNECTION_SECURITY_ELEMENT = "connection-security";
+ public static final String AUTHENTICATION_TYPE_ELEMENT = "authentication-type";
+ public static final String USERNAME_ELEMENT = "username";
+ public static final String PASSWORD_ELEMENT = "password";
+ public static final String EXTRA_ELEMENT = "extra";
+ public static final String IDENTITIES_ELEMENT = "identities";
+ public static final String IDENTITY_ELEMENT = "identity";
+ public static final String FOLDERS_ELEMENT = "folders";
+ public static final String FOLDER_ELEMENT = "folder";
+ public static final String NAME_ATTRIBUTE = "name";
+ public static final String VALUE_ELEMENT = "value";
+ public static final String KEY_ATTRIBUTE = "key";
+ public static final String NAME_ELEMENT = "name";
+ public static final String EMAIL_ELEMENT = "email";
+ public static final String DESCRIPTION_ELEMENT = "description";
+
+
+ public static String exportToFile(Context context, boolean includeGlobals,
+ Set accountUuids)
+ throws SettingsImportExportException {
+
+ OutputStream os = null;
+ String filename = null;
+ try
+ {
+ File dir = new File(Environment.getExternalStorageDirectory() + File.separator
+ + context.getPackageName());
+ dir.mkdirs();
+ File file = Utility.createUniqueFile(dir, EXPORT_FILENAME);
+ filename = file.getAbsolutePath();
+ os = new FileOutputStream(filename);
+
+ exportPreferences(context, os, includeGlobals, accountUuids);
+
+ // If all went well, we return the name of the file just written.
+ return filename;
+ } catch (Exception e) {
+ throw new SettingsImportExportException(e);
+ } finally {
+ if (os != null) {
+ try {
+ os.close();
+ } catch (IOException ioe) {
+ Log.w(K9.LOG_TAG, "Couldn't close exported settings file: " + filename);
+ }
+ }
+ }
+ }
+
+ public static void exportPreferences(Context context, OutputStream os, boolean includeGlobals,
+ Set accountUuids) throws SettingsImportExportException {
+
+ try {
+ XmlSerializer serializer = Xml.newSerializer();
+ serializer.setOutput(os, "UTF-8");
+
+ serializer.startDocument(null, Boolean.valueOf(true));
+
+ // Output with indentation
+ serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
+
+ serializer.startTag(null, ROOT_ELEMENT);
+ serializer.attribute(null, VERSION_ATTRIBUTE, Integer.toString(Settings.VERSION));
+ serializer.attribute(null, FILE_FORMAT_ATTRIBUTE,
+ Integer.toString(FILE_FORMAT_VERSION));
+
+ Log.i(K9.LOG_TAG, "Exporting preferences");
+
+ Preferences preferences = Preferences.getPreferences(context);
+ SharedPreferences storage = preferences.getPreferences();
+
+ Set exportAccounts;
+ if (accountUuids == null) {
+ Account[] accounts = preferences.getAccounts();
+ exportAccounts = new HashSet();
+ for (Account account : accounts) {
+ exportAccounts.add(account.getUuid());
+ }
+ } else {
+ exportAccounts = accountUuids;
+ }
+
+ Map prefs = new TreeMap(storage.getAll());
+
+ if (includeGlobals) {
+ serializer.startTag(null, GLOBAL_ELEMENT);
+ writeSettings(serializer, prefs);
+ serializer.endTag(null, GLOBAL_ELEMENT);
+ }
+
+ serializer.startTag(null, ACCOUNTS_ELEMENT);
+ for (String accountUuid : exportAccounts) {
+ Account account = preferences.getAccount(accountUuid);
+ writeAccount(serializer, account, prefs);
+ }
+ serializer.endTag(null, ACCOUNTS_ELEMENT);
+
+ serializer.endTag(null, ROOT_ELEMENT);
+ serializer.endDocument();
+ serializer.flush();
+
+ } catch (Exception e) {
+ throw new SettingsImportExportException(e.getLocalizedMessage(), e);
+ }
+ }
+
+ private static void writeSettings(XmlSerializer serializer,
+ Map prefs) throws IOException {
+
+ for (String key : GlobalSettings.SETTINGS.keySet()) {
+ String valueString = (String) prefs.get(key);
+ if (valueString != null) {
+ try {
+ SettingsDescription setting = GlobalSettings.SETTINGS.get(key);
+ Object value = setting.fromString(valueString);
+ String outputValue = setting.toPrettyString(value);
+ writeKeyValue(serializer, key, outputValue);
+ } catch (InvalidSettingValueException e) {
+ Log.w(K9.LOG_TAG, "Global setting \"" + key + "\" has invalid value \"" +
+ valueString + "\" in preference storage. This shouldn't happen!");
+ }
+ } else {
+ if (K9.DEBUG) {
+ Log.d(K9.LOG_TAG, "Couldn't find key \"" + key + "\" in preference storage." +
+ "Using default value.");
+ }
+
+ SettingsDescription setting = GlobalSettings.SETTINGS.get(key);
+ Object value = setting.getDefaultValue();
+ String outputValue = setting.toPrettyString(value);
+ writeKeyValue(serializer, key, outputValue);
+ }
+ }
+ }
+
+ private static void writeAccount(XmlSerializer serializer, Account account,
+ Map prefs) throws IOException {
+
+ Set identities = new HashSet();
+ Set folders = new HashSet();
+ String accountUuid = account.getUuid();
+
+ serializer.startTag(null, ACCOUNT_ELEMENT);
+ serializer.attribute(null, UUID_ATTRIBUTE, accountUuid);
+
+ String name = (String) prefs.get(accountUuid + "." + Account.ACCOUNT_DESCRIPTION_KEY);
+ if (name != null) {
+ serializer.startTag(null, NAME_ELEMENT);
+ serializer.text(name);
+ serializer.endTag(null, NAME_ELEMENT);
+ }
+
+
+ // Write incoming server settings
+ ServerSettings incoming = Store.decodeStoreUri(account.getStoreUri());
+ serializer.startTag(null, INCOMING_SERVER_ELEMENT);
+ serializer.attribute(null, TYPE_ATTRIBUTE, incoming.type);
+
+ writeElement(serializer, HOST_ELEMENT, incoming.host);
+ if (incoming.port != -1) {
+ writeElement(serializer, PORT_ELEMENT, Integer.toString(incoming.port));
+ }
+ writeElement(serializer, CONNECTION_SECURITY_ELEMENT, incoming.connectionSecurity.name());
+ writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, incoming.authenticationType);
+ writeElement(serializer, USERNAME_ELEMENT, incoming.username);
+ // XXX For now we don't export the password
+ //writeElement(serializer, PASSWORD_ELEMENT, incoming.password);
+
+ Map extras = incoming.getExtra();
+ if (extras != null && extras.size() > 0) {
+ serializer.startTag(null, EXTRA_ELEMENT);
+ for (Entry extra : extras.entrySet()) {
+ writeKeyValue(serializer, extra.getKey(), extra.getValue());
+ }
+ serializer.endTag(null, EXTRA_ELEMENT);
+ }
+
+ serializer.endTag(null, INCOMING_SERVER_ELEMENT);
+
+
+ // Write outgoing server settings
+ ServerSettings outgoing = Transport.decodeTransportUri(account.getTransportUri());
+ serializer.startTag(null, OUTGOING_SERVER_ELEMENT);
+ serializer.attribute(null, TYPE_ATTRIBUTE, outgoing.type);
+
+ writeElement(serializer, HOST_ELEMENT, outgoing.host);
+ if (outgoing.port != -1) {
+ writeElement(serializer, PORT_ELEMENT, Integer.toString(outgoing.port));
+ }
+ writeElement(serializer, CONNECTION_SECURITY_ELEMENT, outgoing.connectionSecurity.name());
+ writeElement(serializer, AUTHENTICATION_TYPE_ELEMENT, outgoing.authenticationType);
+ writeElement(serializer, USERNAME_ELEMENT, outgoing.username);
+ // XXX For now we don't export the password
+ //writeElement(serializer, PASSWORD_ELEMENT, outgoing.password);
+
+ extras = outgoing.getExtra();
+ if (extras != null && extras.size() > 0) {
+ serializer.startTag(null, EXTRA_ELEMENT);
+ for (Entry extra : extras.entrySet()) {
+ writeKeyValue(serializer, extra.getKey(), extra.getValue());
+ }
+ serializer.endTag(null, EXTRA_ELEMENT);
+ }
+
+ serializer.endTag(null, OUTGOING_SERVER_ELEMENT);
+
+
+ // Write account settings
+ serializer.startTag(null, SETTINGS_ELEMENT);
+ for (Map.Entry entry : prefs.entrySet()) {
+ String key = entry.getKey();
+ String valueString = entry.getValue().toString();
+ String[] comps = key.split("\\.");
+
+ if (comps.length < 2) {
+ // Skip global settings
+ continue;
+ }
+
+ String keyUuid = comps[0];
+ String secondPart = comps[1];
+
+ if (!keyUuid.equals(accountUuid)) {
+ // Setting doesn't belong to the account we're currently writing.
+ continue;
+ }
+
+ String keyPart;
+ if (comps.length >= 3) {
+ String thirdPart = comps[2];
+
+ if (Account.IDENTITY_DESCRIPTION_KEY.equals(secondPart)) {
+ // This is an identity key. Save identity index for later...
+ try {
+ identities.add(Integer.parseInt(thirdPart));
+ } catch (NumberFormatException e) { /* ignore */ }
+ // ... but don't write it now.
+ continue;
+ }
+
+ if (FolderSettings.SETTINGS.containsKey(thirdPart)) {
+ // This is a folder key. Save folder name for later...
+ folders.add(secondPart);
+ // ... but don't write it now.
+ continue;
+ }
+
+ // Strip account UUID from key
+ keyPart = key.substring(comps[0].length() + 1);
+ } else {
+ keyPart = secondPart;
+ }
+
+ SettingsDescription setting = AccountSettings.SETTINGS.get(keyPart);
+ if (setting != null) {
+ // Only export account settings that can be found in AccountSettings.SETTINGS
+ try {
+ Object value = setting.fromString(valueString);
+ String pretty = setting.toPrettyString(value);
+ writeKeyValue(serializer, keyPart, pretty);
+ } catch (InvalidSettingValueException e) {
+ Log.w(K9.LOG_TAG, "Account setting \"" + keyPart + "\" (" +
+ account.getDescription() + ") has invalid value \"" + valueString +
+ "\" in preference storage. This shouldn't happen!");
+ }
+ }
+ }
+ serializer.endTag(null, SETTINGS_ELEMENT);
+
+ if (identities.size() > 0) {
+ serializer.startTag(null, IDENTITIES_ELEMENT);
+
+ // Sort identity indices (that's why we store them as Integers)
+ List sortedIdentities = new ArrayList(identities);
+ Collections.sort(sortedIdentities);
+
+ for (Integer identityIndex : sortedIdentities) {
+ writeIdentity(serializer, accountUuid, identityIndex.toString(), prefs);
+ }
+ serializer.endTag(null, IDENTITIES_ELEMENT);
+ }
+
+ if (folders.size() > 0) {
+ serializer.startTag(null, FOLDERS_ELEMENT);
+ for (String folder : folders) {
+ writeFolder(serializer, accountUuid, folder, prefs);
+ }
+ serializer.endTag(null, FOLDERS_ELEMENT);
+ }
+
+ serializer.endTag(null, ACCOUNT_ELEMENT);
+ }
+
+ private static void writeIdentity(XmlSerializer serializer, String accountUuid,
+ String identity, Map prefs) throws IOException {
+
+ serializer.startTag(null, IDENTITY_ELEMENT);
+
+ String prefix = accountUuid + ".";
+ String suffix = "." + identity;
+
+ // Write name belonging to the identity
+ String name = (String) prefs.get(prefix + Account.IDENTITY_NAME_KEY + suffix);
+ serializer.startTag(null, NAME_ELEMENT);
+ serializer.text(name);
+ serializer.endTag(null, NAME_ELEMENT);
+
+ // Write email address belonging to the identity
+ String email = (String) prefs.get(prefix + Account.IDENTITY_EMAIL_KEY + suffix);
+ serializer.startTag(null, EMAIL_ELEMENT);
+ serializer.text(email);
+ serializer.endTag(null, EMAIL_ELEMENT);
+
+ // Write identity description
+ String description = (String) prefs.get(prefix + Account.IDENTITY_DESCRIPTION_KEY + suffix);
+ if (description != null) {
+ serializer.startTag(null, DESCRIPTION_ELEMENT);
+ serializer.text(description);
+ serializer.endTag(null, DESCRIPTION_ELEMENT);
+ }
+
+ // Write identity settings
+ serializer.startTag(null, SETTINGS_ELEMENT);
+ for (Map.Entry entry : prefs.entrySet()) {
+ String key = entry.getKey();
+ String valueString = entry.getValue().toString();
+ String[] comps = key.split("\\.");
+
+ if (comps.length < 3) {
+ // Skip non-identity config entries
+ continue;
+ }
+
+ String keyUuid = comps[0];
+ String identityKey = comps[1];
+ String identityIndex = comps[2];
+ if (!keyUuid.equals(accountUuid) || !identityIndex.equals(identity)) {
+ // Skip entries that belong to another identity
+ continue;
+ }
+
+ SettingsDescription setting = IdentitySettings.SETTINGS.get(identityKey);
+ if (setting != null) {
+ // Only write settings that have an entry in IdentitySettings.SETTINGS
+ try {
+ Object value = setting.fromString(valueString);
+ String outputValue = setting.toPrettyString(value);
+ writeKeyValue(serializer, identityKey, outputValue);
+ } catch (InvalidSettingValueException e) {
+ Log.w(K9.LOG_TAG, "Identity setting \"" + identityKey +
+ "\" has invalid value \"" + valueString +
+ "\" in preference storage. This shouldn't happen!");
+ }
+ }
+ }
+ serializer.endTag(null, SETTINGS_ELEMENT);
+
+ serializer.endTag(null, IDENTITY_ELEMENT);
+ }
+
+ private static void writeFolder(XmlSerializer serializer, String accountUuid,
+ String folder, Map prefs) throws IOException {
+
+ serializer.startTag(null, FOLDER_ELEMENT);
+ serializer.attribute(null, NAME_ATTRIBUTE, folder);
+
+ // Write folder settings
+ for (Map.Entry entry : prefs.entrySet()) {
+ String key = entry.getKey();
+ String valueString = entry.getValue().toString();
+ String[] comps = key.split("\\.");
+
+ if (comps.length < 3) {
+ // Skip non-folder config entries
+ continue;
+ }
+
+ String keyUuid = comps[0];
+ String folderName = comps[1];
+ String folderKey = comps[2];
+
+ if (!keyUuid.equals(accountUuid) || !folderName.equals(folder)) {
+ // Skip entries that belong to another folder
+ continue;
+ }
+
+ SettingsDescription setting = FolderSettings.SETTINGS.get(folderKey);
+ if (setting != null) {
+ // Only write settings that have an entry in FolderSettings.SETTINGS
+ try {
+ Object value = setting.fromString(valueString);
+ String outputValue = setting.toPrettyString(value);
+ writeKeyValue(serializer, folderKey, outputValue);
+ } catch (InvalidSettingValueException e) {
+ Log.w(K9.LOG_TAG, "Folder setting \"" + folderKey +
+ "\" has invalid value \"" + valueString +
+ "\" in preference storage. This shouldn't happen!");
+ }
+ }
+ }
+
+ serializer.endTag(null, FOLDER_ELEMENT);
+ }
+
+ private static void writeElement(XmlSerializer serializer, String elementName, String value)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ if (value != null) {
+ serializer.startTag(null, elementName);
+ serializer.text(value);
+ serializer.endTag(null, elementName);
+ }
+ }
+
+ private static void writeKeyValue(XmlSerializer serializer, String key, String value)
+ throws IllegalArgumentException, IllegalStateException, IOException {
+ serializer.startTag(null, VALUE_ELEMENT);
+ serializer.attribute(null, KEY_ATTRIBUTE, key);
+ if (value != null) {
+ serializer.text(value);
+ }
+ serializer.endTag(null, VALUE_ELEMENT);
+ }
+}
diff --git a/src/com/fsck/k9/preferences/SettingsImportExportException.java b/src/com/fsck/k9/preferences/SettingsImportExportException.java
new file mode 100644
index 000000000..616a466bf
--- /dev/null
+++ b/src/com/fsck/k9/preferences/SettingsImportExportException.java
@@ -0,0 +1,21 @@
+package com.fsck.k9.preferences;
+
+public class SettingsImportExportException extends Exception {
+
+ public SettingsImportExportException() {
+ super();
+ }
+
+ public SettingsImportExportException(String detailMessage, Throwable throwable) {
+ super(detailMessage, throwable);
+ }
+
+ public SettingsImportExportException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public SettingsImportExportException(Throwable throwable) {
+ super(throwable);
+ }
+
+}
diff --git a/src/com/fsck/k9/preferences/SettingsImporter.java b/src/com/fsck/k9/preferences/SettingsImporter.java
new file mode 100644
index 000000000..3c1b36475
--- /dev/null
+++ b/src/com/fsck/k9/preferences/SettingsImporter.java
@@ -0,0 +1,1106 @@
+package com.fsck.k9.preferences;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.Identity;
+import com.fsck.k9.K9;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.helper.DateFormatter;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.ConnectionSecurity;
+import com.fsck.k9.mail.ServerSettings;
+import com.fsck.k9.mail.Store;
+import com.fsck.k9.mail.Transport;
+import com.fsck.k9.mail.store.WebDavStore;
+import com.fsck.k9.preferences.Settings.InvalidSettingValueException;
+
+public class SettingsImporter {
+
+ /**
+ * Class to list the contents of an import file/stream.
+ *
+ * @see SettingsImporter#getImportStreamContents(InputStream)
+ */
+ public static class ImportContents {
+ /**
+ * True, if the import file contains global settings.
+ */
+ public final boolean globalSettings;
+
+ /**
+ * The list of accounts found in the import file. Never {@code null}.
+ */
+ public final List accounts;
+
+ private ImportContents(boolean globalSettings, List accounts) {
+ this.globalSettings = globalSettings;
+ this.accounts = accounts;
+ }
+ }
+
+ /**
+ * Class to describe an account (name, UUID).
+ *
+ * @see ImportContents
+ */
+ public static class AccountDescription {
+ /**
+ * The name of the account.
+ */
+ public final String name;
+
+ /**
+ * The UUID of the account.
+ */
+ public final String uuid;
+
+ private AccountDescription(String name, String uuid) {
+ this.name = name;
+ this.uuid = uuid;
+ }
+ }
+
+ public static class AccountDescriptionPair {
+ public final AccountDescription original;
+ public final AccountDescription imported;
+
+ private AccountDescriptionPair(AccountDescription original, AccountDescription imported) {
+ this.original = original;
+ this.imported = imported;
+ }
+ }
+
+ public static class ImportResults {
+ public final boolean globalSettings;
+ public final List importedAccounts;
+ public final List errorneousAccounts;
+
+ private ImportResults(boolean globalSettings,
+ List importedAccounts,
+ List errorneousAccounts) {
+ this.globalSettings = globalSettings;
+ this.importedAccounts = importedAccounts;
+ this.errorneousAccounts = errorneousAccounts;
+ }
+ }
+
+ /**
+ * Parses an import {@link InputStream} and returns information on whether it contains global
+ * settings and/or account settings. For all account configurations found, the name of the
+ * account along with the account UUID is returned.
+ *
+ * @param inputStream
+ * An {@code InputStream} to read the settings from.
+ *
+ * @return An {@link ImportContents} instance containing information about the contents of the
+ * settings file.
+ *
+ * @throws SettingsImportExportException
+ * In case of an error.
+ */
+ public static ImportContents getImportStreamContents(InputStream inputStream)
+ throws SettingsImportExportException {
+
+ try {
+ // Parse the import stream but don't save individual settings (overview=true)
+ Imported imported = parseSettings(inputStream, false, null, true);
+
+ // If the stream contains global settings the "globalSettings" member will not be null
+ boolean globalSettings = (imported.globalSettings != null);
+
+ final List accounts = new ArrayList();
+ // If the stream contains at least one account configuration the "accounts" member
+ // will not be null.
+ if (imported.accounts != null) {
+ for (ImportedAccount account : imported.accounts.values()) {
+ accounts.add(new AccountDescription(account.name, account.uuid));
+ }
+ }
+
+ //TODO: throw exception if neither global settings nor account settings could be found
+
+ return new ImportContents(globalSettings, accounts);
+
+ } catch (SettingsImportExportException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SettingsImportExportException(e);
+ }
+ }
+
+ /**
+ * Reads an import {@link InputStream} and imports the global settings and/or account
+ * configurations specified by the arguments.
+ *
+ * @param context
+ * A {@link Context} instance.
+ * @param inputStream
+ * The {@code InputStream} to read the settings from.
+ * @param globalSettings
+ * {@code true} if global settings should be imported from the file.
+ * @param accountUuids
+ * A list of UUIDs of the accounts that should be imported.
+ * @param overwrite
+ * {@code true} if existing accounts should be overwritten when an account with the
+ * same UUID is found in the settings file.
+ * Note: This can have side-effects we currently don't handle, e.g.
+ * changing the account type from IMAP to POP3. So don't use this for now!
+ *
+ * @return An {@link ImportResults} instance containing information about errors and
+ * successfully imported accounts.
+ *
+ * @throws SettingsImportExportException
+ * In case of an error.
+ */
+ public static ImportResults importSettings(Context context, InputStream inputStream,
+ boolean globalSettings, List accountUuids, boolean overwrite)
+ throws SettingsImportExportException {
+
+ try
+ {
+ boolean globalSettingsImported = false;
+ List importedAccounts = new ArrayList();
+ List errorneousAccounts = new ArrayList();
+
+ Imported imported = parseSettings(inputStream, globalSettings, accountUuids, false);
+
+ Preferences preferences = Preferences.getPreferences(context);
+ SharedPreferences storage = preferences.getPreferences();
+
+ if (globalSettings) {
+ try {
+ SharedPreferences.Editor editor = storage.edit();
+ if (imported.globalSettings != null) {
+ importGlobalSettings(storage, editor, imported.globalSettings);
+ } else {
+ Log.w(K9.LOG_TAG, "Was asked to import global settings but none found.");
+ }
+ if (editor.commit()) {
+ if (K9.DEBUG) {
+ Log.v(K9.LOG_TAG, "Committed global settings to the preference " +
+ "storage.");
+ }
+ globalSettingsImported = true;
+ } else {
+ if (K9.DEBUG) {
+ Log.v(K9.LOG_TAG, "Failed to commit global settings to the " +
+ "preference storage");
+ }
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Exception while importing global settings", e);
+ }
+ }
+
+ if (accountUuids != null && accountUuids.size() > 0) {
+ if (imported.accounts != null) {
+ List newUuids = new ArrayList();
+ for (String accountUuid : accountUuids) {
+ if (imported.accounts.containsKey(accountUuid)) {
+ ImportedAccount account = imported.accounts.get(accountUuid);
+ try {
+ SharedPreferences.Editor editor = storage.edit();
+
+ AccountDescriptionPair importResult = importAccount(context,
+ editor, account, overwrite);
+
+ String newUuid = importResult.imported.uuid;
+ if (!newUuid.equals(importResult.original.uuid)) {
+ newUuids.add(newUuid);
+ }
+ if (editor.commit()) {
+ if (K9.DEBUG) {
+ Log.v(K9.LOG_TAG, "Committed settings for account \"" +
+ importResult.imported.name +
+ "\" to the settings database.");
+ }
+ importedAccounts.add(importResult);
+ } else {
+ if (K9.DEBUG) {
+ Log.w(K9.LOG_TAG, "Error while committing settings for " +
+ "account \"" + importResult.original.name +
+ "\" to the settings database.");
+ }
+ errorneousAccounts.add(importResult.original);
+ }
+ } catch (InvalidSettingValueException e) {
+ if (K9.DEBUG) {
+ Log.e(K9.LOG_TAG, "Encountered invalid setting while " +
+ "importing account \"" + account.name + "\"", e);
+ }
+ errorneousAccounts.add(new AccountDescription(account.name, account.uuid));
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "Exception while importing account \"" +
+ account.name + "\"", e);
+ errorneousAccounts.add(new AccountDescription(account.name, account.uuid));
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Was asked to import account with UUID " +
+ accountUuid + ". But this account wasn't found.");
+ }
+ }
+
+ SharedPreferences.Editor editor = storage.edit();
+
+ if (newUuids.size() > 0) {
+ String oldAccountUuids = storage.getString("accountUuids", "");
+ String appendUuids = Utility.combine(newUuids.toArray(new String[0]), ',');
+ String prefix = "";
+ if (oldAccountUuids.length() > 0) {
+ prefix = oldAccountUuids + ",";
+ }
+ putString(editor, "accountUuids", prefix + appendUuids);
+ }
+
+ String defaultAccountUuid = storage.getString("defaultAccountUuid", null);
+ if (defaultAccountUuid == null) {
+ putString(editor, "defaultAccountUuid", accountUuids.get(0));
+ }
+
+ if (!editor.commit()) {
+ throw new SettingsImportExportException("Failed to set default account");
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Was asked to import at least one account but none found.");
+ }
+ }
+
+ preferences.loadAccounts();
+ DateFormatter.clearChosenFormat();
+ K9.loadPrefs(preferences);
+ K9.setServicesEnabled(context);
+
+ return new ImportResults(globalSettingsImported, importedAccounts, errorneousAccounts);
+
+ } catch (SettingsImportExportException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new SettingsImportExportException(e);
+ }
+ }
+
+ private static void importGlobalSettings(SharedPreferences storage,
+ SharedPreferences.Editor editor, ImportedSettings settings) {
+
+ Map validatedSettings = GlobalSettings.validate(settings.settings);
+
+ // Use current global settings as base and overwrite with validated settings read from the
+ // import file.
+ Map mergedSettings =
+ new HashMap(GlobalSettings.getGlobalSettings(storage));
+ mergedSettings.putAll(validatedSettings);
+
+ for (Map.Entry setting : mergedSettings.entrySet()) {
+ String key = setting.getKey();
+ String value = setting.getValue();
+ putString(editor, key, value);
+ }
+ }
+
+ private static AccountDescriptionPair importAccount(Context context,
+ SharedPreferences.Editor editor, ImportedAccount account, boolean overwrite)
+ throws InvalidSettingValueException {
+
+ AccountDescription original = new AccountDescription(account.name, account.uuid);
+
+ Preferences prefs = Preferences.getPreferences(context);
+ Account[] accounts = prefs.getAccounts();
+
+ String uuid = account.uuid;
+ Account existingAccount = prefs.getAccount(uuid);
+ boolean mergeImportedAccount = (overwrite && existingAccount != null);
+
+ if (!overwrite && existingAccount != null) {
+ // An account with this UUID already exists, but we're not allowed to overwrite it.
+ // So generate a new UUID.
+ uuid = UUID.randomUUID().toString();
+ }
+
+ // Make sure the account name is unique
+ String accountName = (account.name != null) ? account.name : "Imported";
+ if (isAccountNameUsed(accountName, accounts)) {
+ // Account name is already in use. So generate a new one by appending " (x)", where x
+ // is the first number >= 1 that results in an unused account name.
+ for (int i = 1; i <= accounts.length; i++) {
+ accountName = account.name + " (" + i + ")";
+ if (!isAccountNameUsed(accountName, accounts)) {
+ break;
+ }
+ }
+ }
+
+ // Write account name
+ String accountKeyPrefix = uuid + ".";
+ putString(editor, accountKeyPrefix + Account.ACCOUNT_DESCRIPTION_KEY, accountName);
+
+ if (account.incoming == null) {
+ // We don't import accounts without incoming server settings
+ throw new InvalidSettingValueException();
+ }
+
+ // Write incoming server settings (storeUri)
+ ServerSettings incoming = new ImportedServerSettings(account.incoming);
+ String storeUri = Store.createStoreUri(incoming);
+ putString(editor, accountKeyPrefix + Account.STORE_URI_KEY, Utility.base64Encode(storeUri));
+
+ // Mark account as disabled if the settings file didn't contain a password
+ boolean createAccountDisabled = (incoming.password == null ||
+ incoming.password.length() == 0);
+
+ if (account.outgoing == null && !WebDavStore.STORE_TYPE.equals(account.incoming.type)) {
+ // All account types except WebDAV need to provide outgoing server settings
+ throw new InvalidSettingValueException();
+ }
+
+ if (account.outgoing != null) {
+ // Write outgoing server settings (transportUri)
+ ServerSettings outgoing = new ImportedServerSettings(account.outgoing);
+ String transportUri = Transport.createTransportUri(outgoing);
+ putString(editor, accountKeyPrefix + Account.TRANSPORT_URI_KEY, Utility.base64Encode(transportUri));
+
+ // Mark account as disabled if the settings file didn't contain a password
+ if (outgoing.password == null || outgoing.password.length() == 0) {
+ createAccountDisabled = true;
+ }
+ }
+
+ // Write key to mark account as disabled if necessary
+ if (createAccountDisabled) {
+ editor.putBoolean(accountKeyPrefix + "enabled", false);
+ }
+
+ // Validate account settings
+ Map validatedSettings =
+ AccountSettings.validate(account.settings.settings, !mergeImportedAccount);
+
+ // Merge account settings if necessary
+ Map writeSettings;
+ if (mergeImportedAccount) {
+ writeSettings = new HashMap(
+ AccountSettings.getAccountSettings(prefs.getPreferences(), uuid));
+ writeSettings.putAll(validatedSettings);
+ } else {
+ writeSettings = validatedSettings;
+ }
+
+ // Write account settings
+ for (Map.Entry setting : writeSettings.entrySet()) {
+ String key = accountKeyPrefix + setting.getKey();
+ String value = setting.getValue();
+ putString(editor, key, value);
+ }
+
+ // If it's a new account generate and write a new "accountNumber"
+ if (!mergeImportedAccount) {
+ int newAccountNumber = Account.generateAccountNumber(prefs);
+ putString(editor, accountKeyPrefix + "accountNumber", Integer.toString(newAccountNumber));
+ }
+
+ // Write identities
+ if (account.identities != null) {
+ importIdentities(editor, uuid, account, overwrite, existingAccount, prefs);
+ } else if (!mergeImportedAccount) {
+ // Require accounts to at least have one identity
+ throw new InvalidSettingValueException();
+ }
+
+ // Write folder settings
+ if (account.folders != null) {
+ for (ImportedFolder folder : account.folders) {
+ importFolder(editor, uuid, folder, mergeImportedAccount, prefs);
+ }
+ }
+
+ //TODO: sync folder settings with localstore?
+
+ AccountDescription imported = new AccountDescription(accountName, uuid);
+ return new AccountDescriptionPair(original, imported);
+ }
+
+ private static void importFolder(SharedPreferences.Editor editor, String uuid,
+ ImportedFolder folder, boolean overwrite, Preferences prefs) {
+
+ // Validate folder settings
+ Map validatedSettings =
+ FolderSettings.validate(folder.settings.settings, !overwrite);
+
+ // Merge folder settings if necessary
+ Map writeSettings;
+ if (overwrite) {
+ writeSettings = FolderSettings.getFolderSettings(prefs.getPreferences(),
+ uuid, folder.name);
+ writeSettings.putAll(validatedSettings);
+ } else {
+ writeSettings = validatedSettings;
+ }
+
+ // Write folder settings
+ String prefix = uuid + "." + folder.name + ".";
+ for (Map.Entry setting : writeSettings.entrySet()) {
+ String key = prefix + setting.getKey();
+ String value = setting.getValue();
+ putString(editor, key, value);
+ }
+ }
+
+ private static void importIdentities(SharedPreferences.Editor editor, String uuid,
+ ImportedAccount account, boolean overwrite, Account existingAccount,
+ Preferences prefs) throws InvalidSettingValueException {
+
+ String accountKeyPrefix = uuid + ".";
+
+ // Gather information about existing identities for this account (if any)
+ int nextIdentityIndex = 0;
+ final List existingIdentities;
+ if (overwrite && existingAccount != null) {
+ existingIdentities = existingAccount.getIdentities();
+ nextIdentityIndex = existingIdentities.size();
+ } else {
+ existingIdentities = new ArrayList();
+ }
+
+ // Write identities
+ for (ImportedIdentity identity : account.identities) {
+ int writeIdentityIndex = nextIdentityIndex;
+ boolean mergeSettings = false;
+ if (overwrite && existingIdentities.size() > 0) {
+ int identityIndex = findIdentity(identity, existingIdentities);
+ if (identityIndex != -1) {
+ writeIdentityIndex = identityIndex;
+ mergeSettings = true;
+ }
+ }
+ if (!mergeSettings) {
+ nextIdentityIndex++;
+ }
+
+ String identityDescription = (identity.description == null) ?
+ "Imported" : identity.description;
+ if (isIdentityDescriptionUsed(identityDescription, existingIdentities)) {
+ // Identity description is already in use. So generate a new one by appending
+ // " (x)", where x is the first number >= 1 that results in an unused identity
+ // description.
+ for (int i = 1; i <= existingIdentities.size(); i++) {
+ identityDescription = identity.description + " (" + i + ")";
+ if (!isIdentityDescriptionUsed(identityDescription, existingIdentities)) {
+ break;
+ }
+ }
+ }
+
+ String identitySuffix = "." + writeIdentityIndex;
+
+ // Write name used in identity
+ String identityName = (identity.name == null) ? "" : identity.name;
+ putString(editor, accountKeyPrefix + Account.IDENTITY_NAME_KEY + identitySuffix,
+ identityName);
+
+ // Validate email address
+ if (!IdentitySettings.isEmailAddressValid(identity.email)) {
+ throw new InvalidSettingValueException();
+ }
+
+ // Write email address
+ putString(editor, accountKeyPrefix + Account.IDENTITY_EMAIL_KEY + identitySuffix,
+ identity.email);
+
+ // Write identity description
+ putString(editor, accountKeyPrefix + Account.IDENTITY_DESCRIPTION_KEY + identitySuffix,
+ identityDescription);
+
+ if (identity.settings != null) {
+ // Validate identity settings
+ Map validatedSettings = IdentitySettings.validate(
+ identity.settings.settings, !mergeSettings);
+
+ // Merge identity settings if necessary
+ Map writeSettings;
+ if (mergeSettings) {
+ writeSettings = new HashMap(IdentitySettings.getIdentitySettings(
+ prefs.getPreferences(), uuid, writeIdentityIndex));
+ writeSettings.putAll(validatedSettings);
+ } else {
+ writeSettings = validatedSettings;
+ }
+
+ // Write identity settings
+ for (Map.Entry setting : writeSettings.entrySet()) {
+ String key = accountKeyPrefix + setting.getKey() + identitySuffix;
+ String value = setting.getValue();
+ putString(editor, key, value);
+ }
+ }
+ }
+ }
+
+ private static boolean isAccountNameUsed(String name, Account[] accounts) {
+ for (Account account : accounts) {
+ if (account.getDescription().equals(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isIdentityDescriptionUsed(String description, List identities) {
+ for (Identity identitiy : identities) {
+ if (identitiy.getDescription().equals(description)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int findIdentity(ImportedIdentity identity,
+ List identities) {
+ for (int i = 0; i < identities.size(); i++) {
+ Identity existingIdentity = identities.get(i);
+ if (existingIdentity.getName().equals(identity.name) &&
+ existingIdentity.getEmail().equals(identity.email)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Write to an {@link SharedPreferences.Editor} while logging what is written if debug logging
+ * is enabled.
+ *
+ * @param editor
+ * The {@code Editor} to write to.
+ * @param key
+ * The name of the preference to modify.
+ * @param value
+ * The new value for the preference.
+ */
+ private static void putString(SharedPreferences.Editor editor, String key, String value) {
+ if (K9.DEBUG) {
+ String outputValue = value;
+ if (!K9.DEBUG_SENSITIVE &&
+ (key.endsWith(".transportUri") || key.endsWith(".storeUri"))) {
+ outputValue = "*sensitive*";
+ }
+ Log.v(K9.LOG_TAG, "Setting " + key + "=" + outputValue);
+ }
+ editor.putString(key, value);
+ }
+
+ private static Imported parseSettings(InputStream inputStream, boolean globalSettings,
+ List accountUuids, boolean overview)
+ throws SettingsImportExportException {
+
+ if (!overview && accountUuids == null) {
+ throw new IllegalArgumentException("Argument 'accountUuids' must not be null.");
+ }
+
+ try {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ //factory.setNamespaceAware(true);
+ XmlPullParser xpp = factory.newPullParser();
+
+ InputStreamReader reader = new InputStreamReader(inputStream);
+ xpp.setInput(reader);
+
+ Imported imported = null;
+ int eventType = xpp.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if(eventType == XmlPullParser.START_TAG) {
+ if (SettingsExporter.ROOT_ELEMENT.equals(xpp.getName())) {
+ imported = parseRoot(xpp, globalSettings, accountUuids, overview);
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ if (imported == null || (overview && imported.globalSettings == null &&
+ imported.accounts == null)) {
+ throw new SettingsImportExportException("Invalid import data");
+ }
+
+ return imported;
+ } catch (Exception e) {
+ throw new SettingsImportExportException(e);
+ }
+ }
+
+ private static void skipToEndTag(XmlPullParser xpp, String endTag)
+ throws XmlPullParserException, IOException {
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
+ eventType = xpp.next();
+ }
+ }
+
+ private static String getText(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+
+ int eventType = xpp.next();
+ if (eventType != XmlPullParser.TEXT) {
+ return "";
+ }
+ return xpp.getText();
+ }
+
+ private static Imported parseRoot(XmlPullParser xpp, boolean globalSettings,
+ List accountUuids, boolean overview)
+ throws XmlPullParserException, IOException, SettingsImportExportException {
+
+ Imported result = new Imported();
+
+ String fileFormatVersionString = xpp.getAttributeValue(null,
+ SettingsExporter.FILE_FORMAT_ATTRIBUTE);
+ validateFileFormatVersion(fileFormatVersionString);
+
+ String contentVersionString = xpp.getAttributeValue(null,
+ SettingsExporter.VERSION_ATTRIBUTE);
+ validateContentVersion(contentVersionString);
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.ROOT_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.GLOBAL_ELEMENT.equals(element)) {
+ if (overview || globalSettings) {
+ if (result.globalSettings == null) {
+ if (overview) {
+ result.globalSettings = new ImportedSettings();
+ skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
+ } else {
+ result.globalSettings = parseSettings(xpp, SettingsExporter.GLOBAL_ELEMENT);
+ }
+ } else {
+ skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
+ Log.w(K9.LOG_TAG, "More than one global settings element. Only using the first one!");
+ }
+ } else {
+ skipToEndTag(xpp, SettingsExporter.GLOBAL_ELEMENT);
+ Log.i(K9.LOG_TAG, "Skipping global settings");
+ }
+ } else if (SettingsExporter.ACCOUNTS_ELEMENT.equals(element)) {
+ if (result.accounts == null) {
+ result.accounts = parseAccounts(xpp, accountUuids, overview);
+ } else {
+ Log.w(K9.LOG_TAG, "More than one accounts element. Only using the first one!");
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return result;
+ }
+
+ private static int validateFileFormatVersion(String versionString)
+ throws SettingsImportExportException {
+
+ if (versionString == null) {
+ throw new SettingsImportExportException("Missing file format version");
+ }
+
+ int version;
+ try {
+ version = Integer.parseInt(versionString);
+ } catch (NumberFormatException e) {
+ throw new SettingsImportExportException("Invalid file format version: " +
+ versionString);
+ }
+
+ if (version != SettingsExporter.FILE_FORMAT_VERSION) {
+ throw new SettingsImportExportException("Unsupported file format version: " +
+ versionString);
+ }
+
+ return version;
+ }
+
+ private static int validateContentVersion(String versionString)
+ throws SettingsImportExportException {
+
+ if (versionString == null) {
+ throw new SettingsImportExportException("Missing content version");
+ }
+
+ int version;
+ try {
+ version = Integer.parseInt(versionString);
+ } catch (NumberFormatException e) {
+ throw new SettingsImportExportException("Invalid content version: " +
+ versionString);
+ }
+
+ if (version != Settings.VERSION) {
+ throw new SettingsImportExportException("Unsupported content version: " +
+ versionString);
+ }
+
+ return version;
+ }
+
+ private static ImportedSettings parseSettings(XmlPullParser xpp, String endTag)
+ throws XmlPullParserException, IOException {
+
+ ImportedSettings result = null;
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.VALUE_ELEMENT.equals(element)) {
+ String key = xpp.getAttributeValue(null, SettingsExporter.KEY_ATTRIBUTE);
+ String value = getText(xpp);
+
+ if (result == null) {
+ result = new ImportedSettings();
+ }
+
+ if (result.settings.containsKey(key)) {
+ Log.w(K9.LOG_TAG, "Already read key \"" + key + "\". Ignoring value \"" + value + "\"");
+ } else {
+ result.settings.put(key, value);
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return result;
+ }
+
+ private static Map parseAccounts(XmlPullParser xpp,
+ List accountUuids, boolean overview)
+ throws XmlPullParserException, IOException {
+
+ Map accounts = null;
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.ACCOUNTS_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.ACCOUNT_ELEMENT.equals(element)) {
+ if (accounts == null) {
+ accounts = new HashMap();
+ }
+
+ ImportedAccount account = parseAccount(xpp, accountUuids, overview);
+
+ if (account == null) {
+ // Do nothing - parseAccount() already logged a message
+ } else if (!accounts.containsKey(account.uuid)) {
+ accounts.put(account.uuid, account);
+ } else {
+ Log.w(K9.LOG_TAG, "Duplicate account entries with UUID " + account.uuid +
+ ". Ignoring!");
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return accounts;
+ }
+
+ private static ImportedAccount parseAccount(XmlPullParser xpp, List accountUuids,
+ boolean overview)
+ throws XmlPullParserException, IOException {
+
+ String uuid = xpp.getAttributeValue(null, SettingsExporter.UUID_ATTRIBUTE);
+
+ try {
+ UUID.fromString(uuid);
+ } catch (Exception e) {
+ skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT);
+ Log.w(K9.LOG_TAG, "Skipping account with invalid UUID " + uuid);
+ return null;
+ }
+
+ ImportedAccount account = new ImportedAccount();
+ account.uuid = uuid;
+
+ if (overview || accountUuids.contains(uuid)) {
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.ACCOUNT_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.NAME_ELEMENT.equals(element)) {
+ account.name = getText(xpp);
+ } else if (SettingsExporter.INCOMING_SERVER_ELEMENT.equals(element)) {
+ if (overview) {
+ skipToEndTag(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT);
+ } else {
+ account.incoming = parseServerSettings(xpp, SettingsExporter.INCOMING_SERVER_ELEMENT);
+ }
+ } else if (SettingsExporter.OUTGOING_SERVER_ELEMENT.equals(element)) {
+ if (overview) {
+ skipToEndTag(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT);
+ } else {
+ account.outgoing = parseServerSettings(xpp, SettingsExporter.OUTGOING_SERVER_ELEMENT);
+ }
+ } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) {
+ if (overview) {
+ skipToEndTag(xpp, SettingsExporter.SETTINGS_ELEMENT);
+ } else {
+ account.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT);
+ }
+ } else if (SettingsExporter.IDENTITIES_ELEMENT.equals(element)) {
+ if (overview) {
+ skipToEndTag(xpp, SettingsExporter.IDENTITIES_ELEMENT);
+ } else {
+ account.identities = parseIdentities(xpp);
+ }
+ } else if (SettingsExporter.FOLDERS_ELEMENT.equals(element)) {
+ if (overview) {
+ skipToEndTag(xpp, SettingsExporter.FOLDERS_ELEMENT);
+ } else {
+ account.folders = parseFolders(xpp);
+ }
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+ } else {
+ skipToEndTag(xpp, SettingsExporter.ACCOUNT_ELEMENT);
+ Log.i(K9.LOG_TAG, "Skipping account with UUID " + uuid);
+ }
+
+ return account;
+ }
+
+ private static ImportedServer parseServerSettings(XmlPullParser xpp, String endTag)
+ throws XmlPullParserException, IOException {
+ ImportedServer server = new ImportedServer();
+
+ server.type = xpp.getAttributeValue(null, SettingsExporter.TYPE_ATTRIBUTE);
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG && endTag.equals(xpp.getName()))) {
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.HOST_ELEMENT.equals(element)) {
+ server.host = getText(xpp);
+ } else if (SettingsExporter.PORT_ELEMENT.equals(element)) {
+ server.port = getText(xpp);
+ } else if (SettingsExporter.CONNECTION_SECURITY_ELEMENT.equals(element)) {
+ server.connectionSecurity = getText(xpp);
+ } else if (SettingsExporter.AUTHENTICATION_TYPE_ELEMENT.equals(element)) {
+ server.authenticationType = getText(xpp);
+ } else if (SettingsExporter.USERNAME_ELEMENT.equals(element)) {
+ server.username = getText(xpp);
+ } else if (SettingsExporter.PASSWORD_ELEMENT.equals(element)) {
+ server.password = getText(xpp);
+ } else if (SettingsExporter.EXTRA_ELEMENT.equals(element)) {
+ server.extras = parseSettings(xpp, SettingsExporter.EXTRA_ELEMENT);
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return server;
+ }
+
+ private static List parseIdentities(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ List identities = null;
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.IDENTITIES_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.IDENTITY_ELEMENT.equals(element)) {
+ if (identities == null) {
+ identities = new ArrayList();
+ }
+
+ ImportedIdentity identity = parseIdentity(xpp);
+ identities.add(identity);
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return identities;
+ }
+
+ private static ImportedIdentity parseIdentity(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ ImportedIdentity identity = new ImportedIdentity();
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.IDENTITY_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.NAME_ELEMENT.equals(element)) {
+ identity.name = getText(xpp);
+ } else if (SettingsExporter.EMAIL_ELEMENT.equals(element)) {
+ identity.email = getText(xpp);
+ } else if (SettingsExporter.DESCRIPTION_ELEMENT.equals(element)) {
+ identity.description = getText(xpp);
+ } else if (SettingsExporter.SETTINGS_ELEMENT.equals(element)) {
+ identity.settings = parseSettings(xpp, SettingsExporter.SETTINGS_ELEMENT);
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return identity;
+ }
+
+ private static List parseFolders(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ List folders = null;
+
+ int eventType = xpp.next();
+ while (!(eventType == XmlPullParser.END_TAG &&
+ SettingsExporter.FOLDERS_ELEMENT.equals(xpp.getName()))) {
+
+ if(eventType == XmlPullParser.START_TAG) {
+ String element = xpp.getName();
+ if (SettingsExporter.FOLDER_ELEMENT.equals(element)) {
+ if (folders == null) {
+ folders = new ArrayList();
+ }
+
+ ImportedFolder folder = parseFolder(xpp);
+ folders.add(folder);
+ } else {
+ Log.w(K9.LOG_TAG, "Unexpected start tag: " + xpp.getName());
+ }
+ }
+ eventType = xpp.next();
+ }
+
+ return folders;
+ }
+
+ private static ImportedFolder parseFolder(XmlPullParser xpp)
+ throws XmlPullParserException, IOException {
+ ImportedFolder folder = new ImportedFolder();
+
+ String name = xpp.getAttributeValue(null, SettingsExporter.NAME_ATTRIBUTE);
+ folder.name = name;
+
+ folder.settings = parseSettings(xpp, SettingsExporter.FOLDER_ELEMENT);
+
+ return folder;
+ }
+
+ private static class ImportedServerSettings extends ServerSettings {
+ private final ImportedServer mImportedServer;
+
+ public ImportedServerSettings(ImportedServer server) {
+ super(server.type, server.host, convertPort(server.port),
+ convertConnectionSecurity(server.connectionSecurity),
+ server.authenticationType, server.username, server.password);
+ mImportedServer = server;
+ }
+
+ @Override
+ public Map getExtra() {
+ return (mImportedServer.extras != null) ?
+ Collections.unmodifiableMap(mImportedServer.extras.settings) : null;
+ }
+
+ private static int convertPort(String port) {
+ try {
+ return Integer.parseInt(port);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
+ private static ConnectionSecurity convertConnectionSecurity(String connectionSecurity) {
+ try {
+ return ConnectionSecurity.valueOf(connectionSecurity);
+ } catch (Exception e) {
+ return ConnectionSecurity.NONE;
+ }
+ }
+ }
+
+ private static class Imported {
+ public ImportedSettings globalSettings;
+ public Map accounts;
+ }
+
+ private static class ImportedSettings {
+ public Map settings = new HashMap();
+ }
+
+ private static class ImportedAccount {
+ public String uuid;
+ public String name;
+ public ImportedServer incoming;
+ public ImportedServer outgoing;
+ public ImportedSettings settings;
+ public List identities;
+ public List folders;
+ }
+
+ private static class ImportedServer {
+ public String type;
+ public String host;
+ public String port;
+ public String connectionSecurity;
+ public String authenticationType;
+ public String username;
+ public String password;
+ public ImportedSettings extras;
+ }
+
+ private static class ImportedIdentity {
+ public String name;
+ public String email;
+ public String description;
+ public ImportedSettings settings;
+ }
+
+ private static class ImportedFolder {
+ public String name;
+ public ImportedSettings settings;
+ }
+}
diff --git a/src/com/fsck/k9/preferences/TimePickerPreference.java b/src/com/fsck/k9/preferences/TimePickerPreference.java
index f829cebfc..a2d6592b3 100644
--- a/src/com/fsck/k9/preferences/TimePickerPreference.java
+++ b/src/com/fsck/k9/preferences/TimePickerPreference.java
@@ -20,7 +20,7 @@ public class TimePickerPreference extends DialogPreference implements
/**
* The validation expression for this preference
*/
- private static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]";
+ public static final String VALIDATION_EXPRESSION = "[0-2]*[0-9]:[0-5]*[0-9]";
/**
* The default value for this preference
diff --git a/src/com/fsck/k9/service/MailService.java b/src/com/fsck/k9/service/MailService.java
index 55a668ef1..8f3330073 100644
--- a/src/com/fsck/k9/service/MailService.java
+++ b/src/com/fsck/k9/service/MailService.java
@@ -274,7 +274,7 @@ public class MailService extends CoreService {
}
int shortestInterval = -1;
- for (Account account : prefs.getAccounts()) {
+ for (Account account : prefs.getAvailableAccounts()) {
if (account.getAutomaticCheckIntervalMinutes() != -1 &&
account.getFolderSyncMode() != FolderMode.NONE &&
(account.getAutomaticCheckIntervalMinutes() < shortestInterval ||
@@ -363,7 +363,7 @@ public class MailService extends CoreService {
for (Account account : Preferences.getPreferences(MailService.this).getAccounts()) {
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Setting up pushers for account " + account.getDescription());
- if (account.isAvailable(getApplicationContext())) {
+ if (account.isEnabled() && account.isAvailable(getApplicationContext())) {
pushing |= MessagingController.getInstance(getApplication()).setupPushing(account);
} else {
//TODO: setupPushing of unavailable accounts when they become available (sd-card inserted)