diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 405cedaa8..67654233c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -396,6 +396,11 @@ otherwise it would make K-9 start at the wrong time
android:readPermission="com.fsck.k9.permission.READ_MESSAGES"
android:writePermission="com.fsck.k9.permission.DELETE_MESSAGES"
/>
+
+
+
+
+
-
-
-
diff --git a/res/menu/message_list_option.xml b/res/menu/message_list_option.xml
index 4fef1630c..18bdd61c1 100644
--- a/res/menu/message_list_option.xml
+++ b/res/menu/message_list_option.xml
@@ -34,9 +34,11 @@
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 7b23bf410..5c55ee35d 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1138,6 +1138,7 @@ http://k9mail.googlecode.com/
Sending query to server
Fetching %d results
Fetching %1$d of %2$d results
+ Remote search failed
Search
Enable server search
@@ -1149,4 +1150,7 @@ http://k9mail.googlecode.com/
Use background as (un)read indicator
Show read and unread messages with different background colors
+
+ Threaded view
+ Collapse messages belonging to the same thread
diff --git a/res/xml/global_preferences.xml b/res/xml/global_preferences.xml
index caaf7f190..51bf29284 100644
--- a/res/xml/global_preferences.xml
+++ b/res/xml/global_preferences.xml
@@ -150,6 +150,12 @@
android:summary="@string/global_settings_background_as_unread_indicator_summary"
/>
+
+
mSortAscending = new HashMap();
private static boolean sUseBackgroundAsUnreadIndicator = true;
+ private static boolean sThreadedViewEnabled = true;
+
/**
* The MIME type(s) of attachments we're willing to view.
@@ -473,6 +475,7 @@ public class K9 extends Application {
editor.putString("attachmentdefaultpath", mAttachmentDefaultPath);
editor.putBoolean("useBackgroundAsUnreadIndicator", sUseBackgroundAsUnreadIndicator);
+ editor.putBoolean("threadedView", sThreadedViewEnabled);
fontSizes.save(editor);
}
@@ -643,6 +646,7 @@ public class K9 extends Application {
mAttachmentDefaultPath = sprefs.getString("attachmentdefaultpath", Environment.getExternalStorageDirectory().toString());
sUseBackgroundAsUnreadIndicator = sprefs.getBoolean("useBackgroundAsUnreadIndicator", true);
+ sThreadedViewEnabled = sprefs.getBoolean("threadedView", true);
fontSizes.load(sprefs);
try {
@@ -1138,4 +1142,12 @@ public class K9 extends Application {
public static synchronized void setUseBackgroundAsUnreadIndicator(boolean enabled) {
sUseBackgroundAsUnreadIndicator = enabled;
}
+
+ public static synchronized boolean isThreadedViewEnabled() {
+ return sThreadedViewEnabled;
+ }
+
+ public static synchronized void setThreadedViewEnabled(boolean enable) {
+ sThreadedViewEnabled = enable;
+ }
}
diff --git a/src/com/fsck/k9/SearchAccount.java b/src/com/fsck/k9/SearchAccount.java
deleted file mode 100644
index bf2f25570..000000000
--- a/src/com/fsck/k9/SearchAccount.java
+++ /dev/null
@@ -1,152 +0,0 @@
-package com.fsck.k9;
-
-import java.io.Serializable;
-import java.util.UUID;
-
-import android.content.Context;
-
-import com.fsck.k9.mail.Flag;
-
-/**
- * This is a meta-Account that represents one or more accounts with filters on them. The filter specification
- * is defined by {@link com.fsck.k9.activity.SearchModifier}.
- */
-public class SearchAccount implements BaseAccount, SearchSpecification, Serializable {
- /**
- * Create a {@code SearchAccount} instance for the Unified Inbox.
- *
- * @param context
- * A {@link Context} instance that will be used to get localized strings and will be
- * passed on to the {@code SearchAccount} instance.
- *
- * @return The {@link SearchAccount} instance for the Unified Inbox.
- */
- public static SearchAccount createUnifiedInboxAccount(Context context) {
- SearchAccount unifiedInbox = new SearchAccount(context, true, null, null);
- unifiedInbox.setDescription(context.getString(R.string.integrated_inbox_title));
- unifiedInbox.setEmail(context.getString(R.string.integrated_inbox_detail));
- return unifiedInbox;
- }
-
- /**
- * Create a {@code SearchAccount} instance for the special account "All messages".
- *
- * @param context
- * A {@link Context} instance that will be used to get localized strings and will be
- * passed on to the {@code SearchAccount} instance.
- *
- * @return The {@link SearchAccount} instance for the Unified Inbox.
- */
- public static SearchAccount createAllMessagesAccount(Context context) {
- SearchAccount allMessages = new SearchAccount(context, false, null, null);
- allMessages.setDescription(context.getString(R.string.search_all_messages_title));
- allMessages.setEmail(context.getString(R.string.search_all_messages_detail));
- return allMessages;
- }
-
-
- private static final long serialVersionUID = -4388420303235543976L;
- private Flag[] mRequiredFlags = null;
- private Flag[] mForbiddenFlags = null;
- private String email = null;
- private String description = null;
- private String query = "";
- private boolean integrate = false;
- private String mUuid = null;
- private boolean builtin = false;
- private String[] accountUuids = null;
- private String[] folderNames = null;
-
- public SearchAccount(Preferences preferences) {
- }
-
- protected synchronized void delete(Preferences preferences) {
- }
-
- public synchronized void save(Preferences preferences) {
- }
-
- public SearchAccount(Context context, boolean nintegrate, Flag[] requiredFlags, Flag[] forbiddenFlags) {
- mRequiredFlags = requiredFlags;
- mForbiddenFlags = forbiddenFlags;
- integrate = nintegrate;
- }
-
- @Override
- public synchronized String getEmail() {
- return email;
- }
-
- @Override
- public synchronized void setEmail(String email) {
- this.email = email;
- }
-
- public Flag[] getRequiredFlags() {
- return mRequiredFlags;
- }
-
- public Flag[] getForbiddenFlags() {
- return mForbiddenFlags;
- }
-
- public boolean isIntegrate() {
- return integrate;
- }
-
- public String getDescription() {
- return description;
- }
-
- public void setDescription(String description) {
- this.description = description;
- }
-
- public String getQuery() {
- return query;
- }
-
- public void setQuery(String query) {
- this.query = query;
- }
-
- public String getUuid() {
- if (mUuid == null) {
- setUuid(UUID.randomUUID().toString());
- }
- return mUuid;
- }
-
- public void setUuid(String nUuid) {
- mUuid = nUuid;
- }
-
- public void setIntegrate(boolean integrate) {
- this.integrate = integrate;
- }
-
- public boolean isBuiltin() {
- return builtin;
- }
-
- public void setBuiltin(boolean builtin) {
- this.builtin = builtin;
- }
-
- public String[] getAccountUuids() {
- return accountUuids;
- }
-
- public void setAccountUuids(String[] accountUuids) {
- this.accountUuids = accountUuids;
- }
-
- @Override
- public String[] getFolderNames() {
- return folderNames;
- }
-
- public void setFolderNames(String[] folderNames) {
- this.folderNames = folderNames;
- }
-}
diff --git a/src/com/fsck/k9/SearchSpecification.java b/src/com/fsck/k9/SearchSpecification.java
deleted file mode 100644
index a1f1de0a8..000000000
--- a/src/com/fsck/k9/SearchSpecification.java
+++ /dev/null
@@ -1,19 +0,0 @@
-
-package com.fsck.k9;
-
-import com.fsck.k9.mail.Flag;
-
-public interface SearchSpecification {
-
- public Flag[] getRequiredFlags();
-
- public Flag[] getForbiddenFlags();
-
- public boolean isIntegrate();
-
- public String getQuery();
-
- public String[] getAccountUuids();
-
- public String[] getFolderNames();
-}
\ No newline at end of file
diff --git a/src/com/fsck/k9/activity/AccountList.java b/src/com/fsck/k9/activity/AccountList.java
index 52c64dbe3..9c17ffd42 100644
--- a/src/com/fsck/k9/activity/AccountList.java
+++ b/src/com/fsck/k9/activity/AccountList.java
@@ -21,7 +21,7 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
-import com.fsck.k9.SearchAccount;
+import com.fsck.k9.search.SearchAccount;
/**
diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index 131d8fb6a..5e04a6ede 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -62,11 +62,8 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
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;
import com.fsck.k9.activity.setup.WelcomeMessage;
@@ -80,6 +77,9 @@ 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.search.LocalSearch;
+import com.fsck.k9.search.SearchAccount;
+import com.fsck.k9.search.SearchModifier;
import com.fsck.k9.view.ColorChip;
import com.fsck.k9.preferences.SettingsExporter;
import com.fsck.k9.preferences.SettingsImportExportException;
@@ -126,8 +126,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
private AccountsHandler mHandler = new AccountsHandler();
private AccountsAdapter mAdapter;
- private SearchAccount unreadAccount = null;
- private SearchAccount integratedInboxAccount = null;
+ private SearchAccount mAllMessagesAccount = null;
+ private SearchAccount mUnifiedInboxAccount = null;
private FontSizes mFontSizes = K9.getFontSizes();
private MenuItem mRefreshMenuItem;
@@ -375,7 +375,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
boolean startup = intent.getBooleanExtra(EXTRA_STARTUP, true);
if (startup && K9.startIntegratedInbox() && !K9.isHideSpecialAccounts()) {
- onOpenAccount(integratedInboxAccount);
+ onOpenAccount(mUnifiedInboxAccount);
finish();
return;
} else if (startup && accounts.length == 1 && onOpenAccount(accounts[0])) {
@@ -424,8 +424,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
* Creates and initializes the special accounts ('Unified Inbox' and 'All Messages')
*/
private void createSpecialAccounts() {
- integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
- unreadAccount = SearchAccount.createAllMessagesAccount(this);
+ mUnifiedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
+ mAllMessagesAccount = SearchAccount.createAllMessagesAccount(this);
}
@SuppressWarnings("unchecked")
@@ -519,14 +519,14 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
List newAccounts;
if (!K9.isHideSpecialAccounts() && accounts.length > 0) {
- if (integratedInboxAccount == null || unreadAccount == null) {
+ if (mUnifiedInboxAccount == null || mAllMessagesAccount == null) {
createSpecialAccounts();
}
newAccounts = new ArrayList(accounts.length +
SPECIAL_ACCOUNTS_COUNT);
- newAccounts.add(integratedInboxAccount);
- newAccounts.add(unreadAccount);
+ newAccounts.add(mUnifiedInboxAccount);
+ newAccounts.add(mAllMessagesAccount);
} else {
newAccounts = new ArrayList(accounts.length);
}
@@ -550,7 +550,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
pendingWork.put(account, "true");
final SearchAccount searchAccount = (SearchAccount)account;
- MessagingController.getInstance(getApplication()).searchLocalMessages(searchAccount, null, new MessagingListener() {
+ MessagingController.getInstance(getApplication())
+ .searchLocalMessages(searchAccount.getRelatedSearch(), new MessagingListener() {
@Override
public void searchStats(AccountStats stats) {
mListener.accountStatusChanged(searchAccount, stats);
@@ -607,7 +608,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
private boolean onOpenAccount(BaseAccount account) {
if (account instanceof SearchAccount) {
SearchAccount searchAccount = (SearchAccount)account;
- MessageList.actionHandle(this, searchAccount.getDescription(), searchAccount);
+ MessageList.actionDisplaySearch(this, searchAccount.getRelatedSearch(), false, false);
} else {
Account realAccount = (Account)account;
if (!realAccount.isEnabled()) {
@@ -624,8 +625,10 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName())) {
FolderList.actionHandleAccount(this, realAccount);
} else {
- MessageList.actionHandleFolder(this, realAccount, realAccount.getAutoExpandFolderName());
- }
+ LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName());
+ search.addAllowedFolder(realAccount.getAutoExpandFolderName());
+ search.addAccountUuid(realAccount.getUuid());
+ MessageList.actionDisplaySearch(this, search, false, true);}
}
return true;
}
@@ -1782,49 +1785,20 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
}
@Override
public void onClick(View v) {
- String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId));
+ final String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId));
+ LocalSearch search = null;
+
if (account instanceof SearchAccount) {
- SearchAccount searchAccount = (SearchAccount)account;
-
- MessageList.actionHandle(Accounts.this,
- description, "", searchAccount.isIntegrate(),
- combine(searchAccount.getRequiredFlags(), searchModifier.requiredFlags),
- combine(searchAccount.getForbiddenFlags(), searchModifier.forbiddenFlags));
+ search = ((SearchAccount) account).getRelatedSearch().clone();
+ search.setName(description);
} else {
- SearchSpecification searchSpec = new SearchSpecification() {
- @Override
- public String[] getAccountUuids() {
- return new String[] { account.getUuid() };
- }
-
- @Override
- public Flag[] getForbiddenFlags() {
- return searchModifier.forbiddenFlags;
- }
-
- @Override
- public String getQuery() {
- return "";
- }
-
- @Override
- public Flag[] getRequiredFlags() {
- return searchModifier.requiredFlags;
- }
-
- @Override
- public boolean isIntegrate() {
- return false;
- }
-
- @Override
- public String[] getFolderNames() {
- return null;
- }
-
- };
- MessageList.actionHandle(Accounts.this, description, searchSpec);
+ search = new LocalSearch(description);
+ search.addAccountUuid(account.getUuid());
}
+
+ search.allRequiredFlags(searchModifier.requiredFlags);
+ search.allForbiddenFlags(searchModifier.forbiddenFlags);
+ MessageList.actionDisplaySearch(Accounts.this, search, true, false);
}
}
diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index 2e761c8a0..d72edfbc6 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -52,7 +52,6 @@ import com.fsck.k9.FontSizes;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
-import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.FolderList.FolderListAdapter.FolderListFilter;
import com.fsck.k9.activity.misc.ActionBarNavigationSpinner;
import com.fsck.k9.activity.setup.AccountSettings;
@@ -68,6 +67,9 @@ import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchModifier;
+import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.service.MailService;
/**
@@ -620,7 +622,10 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
}
private void onOpenFolder(String folder) {
- MessageList.actionHandleFolder(this, mAccount, folder);
+ LocalSearch search = new LocalSearch(folder);
+ search.addAccountUuid(mAccount.getUuid());
+ search.addAllowedFolder(folder);
+ MessageList.actionDisplaySearch(this, search, false, false);
}
private void onCompact(Account account) {
@@ -1267,86 +1272,34 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
}
@Override
public void onClick(View v) {
- String description = getString(R.string.search_title,
+ final String description = getString(R.string.search_title,
getString(R.string.message_list_title, account.getDescription(), displayName),
getString(searchModifier.resId));
- SearchSpecification searchSpec = new SearchSpecification() {
- @Override
- public String[] getAccountUuids() {
- return new String[] { account.getUuid() };
- }
-
- @Override
- public Flag[] getForbiddenFlags() {
- return searchModifier.forbiddenFlags;
- }
-
- @Override
- public String getQuery() {
- return "";
- }
-
- @Override
- public Flag[] getRequiredFlags() {
- return searchModifier.requiredFlags;
- }
-
- @Override
- public boolean isIntegrate() {
- return false;
- }
-
- @Override
- public String[] getFolderNames() {
- return new String[] { folderName };
- }
-
- };
- MessageList.actionHandle(FolderList.this, description, searchSpec);
-
+ LocalSearch search = new LocalSearch(description);
+ try {
+ search.allRequiredFlags(searchModifier.requiredFlags);
+ search.allForbiddenFlags(searchModifier.forbiddenFlags);
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ search.addAllowedFolder(folderName);
+ search.addAccountUuid(account.getUuid());
+ MessageList.actionDisplaySearch(FolderList.this, search, true, false);
}
-
}
- private static Flag[] UNREAD_FLAG_ARRAY = { Flag.SEEN };
-
private void openUnreadSearch(Context context, final Account account) {
String description = getString(R.string.search_title, mAccount.getDescription(), getString(R.string.unread_modifier));
-
- SearchSpecification searchSpec = new SearchSpecification() {
- //interface has no override @Override
- public String[] getAccountUuids() {
- return new String[] { account.getUuid() };
- }
-
- //interface has no override @Override
- public Flag[] getForbiddenFlags() {
- return UNREAD_FLAG_ARRAY;
- }
-
- //interface has no override @Override
- public String getQuery() {
- return "";
- }
-
- @Override
- public Flag[] getRequiredFlags() {
- return null;
- }
-
- @Override
- public boolean isIntegrate() {
- return false;
- }
-
- @Override
- public String[] getFolderNames() {
- return null;
- }
-
- };
- MessageList.actionHandle(context, description, searchSpec);
+ LocalSearch search = new LocalSearch(description);
+ search.addAccountUuid(account.getUuid());
+ try {
+ search.allRequiredFlags(new Flag[]{Flag.SEEN});
+ } catch (Exception e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
}
}
diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java
index 415689057..94e6a4ff8 100644
--- a/src/com/fsck/k9/activity/LauncherShortcuts.java
+++ b/src/com/fsck/k9/activity/LauncherShortcuts.java
@@ -7,7 +7,7 @@ import android.os.Parcelable;
import com.fsck.k9.Account;
import com.fsck.k9.BaseAccount;
import com.fsck.k9.R;
-import com.fsck.k9.SearchSpecification;
+import com.fsck.k9.search.SearchSpecification;
public class LauncherShortcuts extends AccountList {
@Override
@@ -31,8 +31,8 @@ public class LauncherShortcuts extends AccountList {
Intent shortcutIntent = null;
if (account instanceof SearchSpecification) {
- shortcutIntent = MessageList.actionHandleAccountIntent(this, account.getDescription(),
- (SearchSpecification) account);
+ shortcutIntent = MessageList.intentDisplaySearch(this, (SearchSpecification) account,
+ false, true, true);
} else {
shortcutIntent = FolderList.actionHandleAccountIntent(this, (Account) account, null,
true);
diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 49211c2c1..c5ad31281 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -24,17 +24,19 @@ import com.fsck.k9.Account.SortType;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
-import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.misc.SwipeGestureDetector.OnSwipeGestureListener;
import com.fsck.k9.activity.setup.AccountSettings;
import com.fsck.k9.activity.setup.FolderSettings;
import com.fsck.k9.activity.setup.Prefs;
import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.fragment.MessageListFragment.MessageListFragmentListener;
-import com.fsck.k9.helper.Utility;
-import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.store.StorageManager;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchSpecification;
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+import com.fsck.k9.search.SearchSpecification.SearchCondition;
/**
@@ -44,86 +46,42 @@ import com.fsck.k9.mail.store.StorageManager;
*/
public class MessageList extends K9FragmentActivity implements MessageListFragmentListener,
OnBackStackChangedListener, OnSwipeGestureListener {
- private static final String EXTRA_ACCOUNT = "account";
- private static final String EXTRA_FOLDER = "folder";
+
+ // for this activity
+ private static final String EXTRA_SEARCH = "search";
+ private static final String EXTRA_NO_THREADING = "no_threading";
+
+ // used for remote search
private static final String EXTRA_SEARCH_ACCOUNT = "com.fsck.k9.search_account";
private static final String EXTRA_SEARCH_FOLDER = "com.fsck.k9.search_folder";
- private static final String EXTRA_QUERY_FLAGS = "queryFlags";
- private static final String EXTRA_FORBIDDEN_FLAGS = "forbiddenFlags";
- private static final String EXTRA_INTEGRATE = "integrate";
- private static final String EXTRA_ACCOUNT_UUIDS = "accountUuids";
- private static final String EXTRA_FOLDER_NAMES = "folderNames";
- private static final String EXTRA_TITLE = "title";
-
- public static void actionHandleFolder(Context context, Account account, String folder) {
- Intent intent = new Intent(context, MessageList.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- intent.putExtra(EXTRA_ACCOUNT, account.getUuid());
-
- if (folder != null) {
- intent.putExtra(EXTRA_FOLDER, folder);
- }
- context.startActivity(intent);
+ public static void actionDisplaySearch(Context context, SearchSpecification search,
+ boolean noThreading, boolean newTask) {
+ actionDisplaySearch(context, search, noThreading, newTask, true);
}
- public static Intent actionHandleFolderIntent(Context context, Account account, String folder) {
- Intent intent = new Intent(context, MessageList.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP |
- Intent.FLAG_ACTIVITY_SINGLE_TOP);
- intent.putExtra(EXTRA_ACCOUNT, account.getUuid());
-
- if (folder != null) {
- intent.putExtra(EXTRA_FOLDER, folder);
- }
- return intent;
+ public static void actionDisplaySearch(Context context, SearchSpecification search,
+ boolean noThreading, boolean newTask, boolean clearTop) {
+ context.startActivity(
+ intentDisplaySearch(context, search, noThreading, newTask, clearTop));
}
- public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) {
+ public static Intent intentDisplaySearch(Context context, SearchSpecification search,
+ boolean noThreading, boolean newTask, boolean clearTop) {
Intent intent = new Intent(context, MessageList.class);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
- intent.putExtra(SearchManager.QUERY, queryString);
- if (flags != null) {
- intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(flags, ','));
- }
- if (forbiddenFlags != null) {
- intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(forbiddenFlags, ','));
- }
- intent.putExtra(EXTRA_INTEGRATE, integrate);
- intent.putExtra(EXTRA_TITLE, title);
- context.startActivity(intent);
- }
+ intent.putExtra(EXTRA_SEARCH, search);
+ intent.putExtra(EXTRA_NO_THREADING, noThreading);
- /**
- * Creates and returns an intent that opens Unified Inbox or All Messages screen.
- */
- public static Intent actionHandleAccountIntent(Context context, String title,
- SearchSpecification searchSpecification) {
- Intent intent = new Intent(context, MessageList.class);
- intent.putExtra(SearchManager.QUERY, searchSpecification.getQuery());
- if (searchSpecification.getRequiredFlags() != null) {
- intent.putExtra(EXTRA_QUERY_FLAGS, Utility.combine(searchSpecification.getRequiredFlags(), ','));
+ if (clearTop) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
}
- if (searchSpecification.getForbiddenFlags() != null) {
- intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(searchSpecification.getForbiddenFlags(), ','));
+ if (newTask) {
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
- intent.putExtra(EXTRA_INTEGRATE, searchSpecification.isIntegrate());
- intent.putExtra(EXTRA_ACCOUNT_UUIDS, searchSpecification.getAccountUuids());
- intent.putExtra(EXTRA_FOLDER_NAMES, searchSpecification.getFolderNames());
- intent.putExtra(EXTRA_TITLE, title);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
return intent;
}
- public static void actionHandle(Context context, String title,
- SearchSpecification searchSpecification) {
- Intent intent = actionHandleAccountIntent(context, title, searchSpecification);
- context.startActivity(intent);
- }
-
private StorageManager.StorageListener mStorageListener = new StorageListenerImplementation();
@@ -131,31 +89,28 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
private TextView mActionBarTitle;
private TextView mActionBarSubTitle;
private TextView mActionBarUnread;
- private String mTitle;
private Menu mMenu;
private MessageListFragment mMessageListFragment;
private Account mAccount;
- private String mQueryString;
private String mFolderName;
- private Flag[] mQueryFlags;
- private Flag[] mForbiddenFlags;
- private String mSearchAccount = null;
- private String mSearchFolder = null;
- private boolean mIntegrate;
- private String[] mAccountUuids;
- private String[] mFolderNames;
+ private LocalSearch mSearch;
+ private boolean mSingleFolderMode;
+ private boolean mSingleAccountMode;
+ /**
+ * {@code true} if the message list should be displayed as flat list (i.e. no threading)
+ * regardless whether or not message threading was enabled in the settings. This is used for
+ * filtered views, e.g. when only displaying the unread messages in a folder.
+ */
+ private boolean mNoThreading;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.message_list);
- // need this for actionbar initialization
- mQueryString = getIntent().getStringExtra(SearchManager.QUERY);
-
mActionBar = getSupportActionBar();
initializeActionBar();
@@ -171,76 +126,63 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
if (mMessageListFragment == null) {
FragmentTransaction ft = fragmentManager.beginTransaction();
- if (mQueryString == null) {
- mMessageListFragment = MessageListFragment.newInstance(mAccount, mFolderName);
- } else if (mSearchAccount != null) {
- mMessageListFragment = MessageListFragment.newInstance(mSearchAccount,
- mSearchFolder, mQueryString, false);
- } else {
- mMessageListFragment = MessageListFragment.newInstance(mTitle, mAccountUuids,
- mFolderNames, mQueryString, mQueryFlags, mForbiddenFlags, mIntegrate);
- }
+ mMessageListFragment = MessageListFragment.newInstance(mSearch,
+ (K9.isThreadedViewEnabled() && !mNoThreading));
ft.add(R.id.message_list_container, mMessageListFragment);
ft.commit();
}
}
private void decodeExtras(Intent intent) {
- mQueryString = intent.getStringExtra(SearchManager.QUERY);
- mFolderName = null;
- mSearchAccount = null;
- mSearchFolder = null;
- if (mQueryString != null) {
+ // check if this intent comes from the system search ( remote )
+ if (intent.getStringExtra(SearchManager.QUERY) != null) {
if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
//Query was received from Search Dialog
+ String query = intent.getStringExtra(SearchManager.QUERY);
+
+ mSearch = new LocalSearch(getString(R.string.search_results));
+ mSearch.setManualSearch(true);
+ mNoThreading = true;
+
+ mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query));
+ mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query));
+ mSearch.or(new SearchCondition(Searchfield.MESSAGE_CONTENTS, Attribute.CONTAINS, query));
+
Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
if (appData != null) {
- mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT);
- mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER);
+ mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT));
+ mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER));
+ } else {
+ mSearch.addAccountUuid(LocalSearch.ALL_ACCOUNTS);
}
- } else {
- mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT);
- mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER);
+ }
+ } else {
+ // regular LocalSearch object was passed
+ mSearch = intent.getParcelableExtra(EXTRA_SEARCH);
+ mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false);
+ }
+
+ String[] accountUuids = mSearch.getAccountUuids();
+ mSingleAccountMode = (accountUuids.length == 1 && !mSearch.searchAllAccounts());
+ mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1);
+
+ if (mSingleAccountMode) {
+ Preferences prefs = Preferences.getPreferences(getApplicationContext());
+ mAccount = prefs.getAccount(accountUuids[0]);
+
+ if (mAccount != null && !mAccount.isAvailable(this)) {
+ Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account");
+ onAccountUnavailable();
+ return;
}
}
- String accountUuid = intent.getStringExtra(EXTRA_ACCOUNT);
- mFolderName = intent.getStringExtra(EXTRA_FOLDER);
-
- mAccount = Preferences.getPreferences(this).getAccount(accountUuid);
-
- if (mAccount != null && !mAccount.isAvailable(this)) {
- Log.i(K9.LOG_TAG, "not opening MessageList of unavailable account");
- onAccountUnavailable();
- return;
+ if (mSingleFolderMode) {
+ mFolderName = mSearch.getFolderNames().get(0);
}
- String queryFlags = intent.getStringExtra(EXTRA_QUERY_FLAGS);
- if (queryFlags != null) {
- String[] flagStrings = queryFlags.split(",");
- mQueryFlags = new Flag[flagStrings.length];
- for (int i = 0; i < flagStrings.length; i++) {
- mQueryFlags[i] = Flag.valueOf(flagStrings[i]);
- }
- }
- String forbiddenFlags = intent.getStringExtra(EXTRA_FORBIDDEN_FLAGS);
- if (forbiddenFlags != null) {
- String[] flagStrings = forbiddenFlags.split(",");
- mForbiddenFlags = new Flag[flagStrings.length];
- for (int i = 0; i < flagStrings.length; i++) {
- mForbiddenFlags[i] = Flag.valueOf(flagStrings[i]);
- }
- }
- mIntegrate = intent.getBooleanExtra(EXTRA_INTEGRATE, false);
- mAccountUuids = intent.getStringArrayExtra(EXTRA_ACCOUNT_UUIDS);
- mFolderNames = intent.getStringArrayExtra(EXTRA_FOLDER_NAMES);
- mTitle = intent.getStringExtra(EXTRA_TITLE);
-
- // Take the initial folder into account only if we are *not* restoring
- // the activity already.
- if (mFolderName == null && mQueryString == null) {
- mFolderName = mAccount.getAutoExpandFolderName();
- }
+ // now we know if we are in single account mode and need a subtitle
+ mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE);
}
@Override
@@ -276,10 +218,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
mActionBarSubTitle = (TextView) customView.findViewById(R.id.actionbar_title_sub);
mActionBarUnread = (TextView) customView.findViewById(R.id.actionbar_unread_count);
- if (mQueryString != null) {
- mActionBarSubTitle.setVisibility(View.GONE);
- }
-
mActionBar.setDisplayHomeAsUpEnabled(true);
}
@@ -407,15 +345,9 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
FragmentManager fragmentManager = getSupportFragmentManager();
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
- } else if (mIntegrate) {
- // If we were in one of the integrated mailboxes (think All Mail or Integrated Inbox), then
- // go to accounts.
- onAccounts();
- } else if (mQueryString != null) {
- // We did a search of some sort. Go back to wherever the user searched from.
+ } else if (!mSingleFolderMode || mMessageListFragment.isManualSearch()) {
onBackPressed();
} else {
- // In a standard message list of a folder. Go to folder list.
onShowFolderList();
}
return true;
@@ -440,10 +372,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
mMessageListFragment.changeSort(SortType.SORT_SUBJECT);
return true;
}
- case R.id.set_sort_sender: {
- mMessageListFragment.changeSort(SortType.SORT_SENDER);
- return true;
- }
+// case R.id.set_sort_sender: {
+// mMessageListFragment.changeSort(SortType.SORT_SENDER);
+// return true;
+// }
case R.id.set_sort_flag: {
mMessageListFragment.changeSort(SortType.SORT_FLAGGED);
return true;
@@ -464,6 +396,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
onEditPrefs();
return true;
}
+ case R.id.account_settings: {
+ onEditAccount();
+ return true;
+ }
case R.id.search: {
mMessageListFragment.onSearchRequested();
return true;
@@ -474,7 +410,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
}
- if (mQueryString != null) {
+ if (!mSingleFolderMode) {
// None of the options after this point are "safe" for search results
//TODO: This is not true for "unread" and "starred" searches in regular folders
return false;
@@ -491,10 +427,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
return true;
}
- case R.id.account_settings: {
- onEditAccount();
- return true;
- }
case R.id.expunge: {
mMessageListFragment.onExpunge();
return true;
@@ -539,22 +471,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
menu.findItem(R.id.select_all).setVisible(true);
menu.findItem(R.id.settings).setVisible(true);
- if (mMessageListFragment.isSearchQuery()) {
+ if (!mSingleAccountMode) {
menu.findItem(R.id.expunge).setVisible(false);
menu.findItem(R.id.check_mail).setVisible(false);
menu.findItem(R.id.send_messages).setVisible(false);
menu.findItem(R.id.folder_settings).setVisible(false);
menu.findItem(R.id.account_settings).setVisible(false);
-
- // If this is an explicit local search, show the option to search the cloud.
- if (!mMessageListFragment.isRemoteSearch() &&
- mMessageListFragment.isRemoteSearchAllowed()) {
- menu.findItem(R.id.search_remote).setVisible(true);
- }
-
} else {
- menu.findItem(R.id.search).setVisible(true);
- menu.findItem(R.id.folder_settings).setVisible(true);
+ menu.findItem(R.id.folder_settings).setVisible(mSingleFolderMode);
menu.findItem(R.id.account_settings).setVisible(true);
if (mMessageListFragment.isOutbox()) {
@@ -571,6 +495,14 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
menu.findItem(R.id.expunge).setVisible(false);
}
}
+
+ // If this is an explicit local search, show the option to search the cloud.
+ if (!mMessageListFragment.isRemoteSearch() &&
+ mMessageListFragment.isRemoteSearchAllowed()) {
+ menu.findItem(R.id.search_remote).setVisible(true);
+ } else if (!mMessageListFragment.isManualSearch()) {
+ menu.findItem(R.id.search).setVisible(true);
+ }
}
}
@@ -672,8 +604,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
@Override
public void showMoreFromSameSender(String senderAddress) {
- MessageListFragment fragment = MessageListFragment.newInstance("From " + senderAddress,
- null, null, senderAddress, null, null, false);
+ LocalSearch tmpSearch = new LocalSearch("From " + senderAddress);
+ tmpSearch.addAccountUuids(mSearch.getAccountUuids());
+ tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS);
+
+ MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
addMessageListFragment(fragment, true);
}
@@ -720,13 +655,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
}
}
- public void remoteSearch(String searchAccount, String searchFolder, String queryString) {
- MessageListFragment fragment = MessageListFragment.newInstance(searchAccount, searchFolder,
- queryString, true);
- mMenu.findItem(R.id.search_remote).setVisible(false);
- addMessageListFragment(fragment, false);
- }
-
private void addMessageListFragment(MessageListFragment fragment, boolean addToBackStack) {
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
@@ -754,4 +682,21 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
return true;
}
+
+ @Override
+ public void showThread(Account account, String folderName, long threadRootId) {
+ LocalSearch tmpSearch = new LocalSearch();
+ tmpSearch.addAccountUuid(account.getUuid());
+ tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS);
+ tmpSearch.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, String.valueOf(threadRootId)));
+
+ MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
+ addMessageListFragment(fragment, true);
+ }
+
+ @Override
+ public void remoteSearchStarted() {
+ // Remove action button for remote search
+ configureMenu(mMenu);
+ }
}
diff --git a/src/com/fsck/k9/activity/SearchModifier.java b/src/com/fsck/k9/activity/SearchModifier.java
deleted file mode 100644
index 8b610ff6e..000000000
--- a/src/com/fsck/k9/activity/SearchModifier.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package com.fsck.k9.activity;
-
-import com.fsck.k9.R;
-import com.fsck.k9.mail.Flag;
-
-/**
- * This enum represents filtering parameters used by {@link com.fsck.k9.SearchAccount}.
- */
-enum SearchModifier {
- FLAGGED(R.string.flagged_modifier, new Flag[]{Flag.FLAGGED}, null),
- UNREAD(R.string.unread_modifier, null, new Flag[]{Flag.SEEN});
-
- final int resId;
- final Flag[] requiredFlags;
- final Flag[] forbiddenFlags;
-
- SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) {
- resId = nResId;
- requiredFlags = nRequiredFlags;
- forbiddenFlags = nForbiddenFlags;
- }
-
-}
\ No newline at end of file
diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java
index c50836d8d..8c29fc2e8 100644
--- a/src/com/fsck/k9/activity/setup/AccountSettings.java
+++ b/src/com/fsck/k9/activity/setup/AccountSettings.java
@@ -499,6 +499,7 @@ public class AccountSettings extends K9PreferenceActivity {
// IMAP-specific preferences
mSearchScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_SEARCH);
+
mCloudSearchEnabled = (CheckBoxPreference) findPreference(PREFERENCE_CLOUD_SEARCH_ENABLED);
mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS);
mRemoteSearchNumResults.setOnPreferenceChangeListener(
diff --git a/src/com/fsck/k9/activity/setup/Prefs.java b/src/com/fsck/k9/activity/setup/Prefs.java
index c878d3f62..0175765e1 100644
--- a/src/com/fsck/k9/activity/setup/Prefs.java
+++ b/src/com/fsck/k9/activity/setup/Prefs.java
@@ -87,8 +87,11 @@ public class Prefs extends K9PreferenceActivity {
private static final String PREFERENCE_ATTACHMENT_DEF_PATH = "attachment_default_path";
private static final String PREFERENCE_BACKGROUND_AS_UNREAD_INDICATOR = "messagelist_background_as_unread_indicator";
+ private static final String PREFERENCE_THREADED_VIEW = "threaded_view";
private static final int ACTIVITY_CHOOSE_FOLDER = 1;
+
+
private ListPreference mLanguage;
private ListPreference mTheme;
private ListPreference mDateFormat;
@@ -128,6 +131,8 @@ public class Prefs extends K9PreferenceActivity {
private CheckBoxPreference mBatchButtonsFlag;
private CheckBoxPreference mBatchButtonsUnselect;
private CheckBoxPreference mBackgroundAsUnreadIndicator;
+ private CheckBoxPreference mThreadedView;
+
public static void actionPrefs(Context context) {
Intent i = new Intent(context, Prefs.class);
@@ -235,6 +240,10 @@ public class Prefs extends K9PreferenceActivity {
mChangeContactNameColor = (CheckBoxPreference)findPreference(PREFERENCE_MESSAGELIST_CONTACT_NAME_COLOR);
mChangeContactNameColor.setChecked(K9.changeContactNameColor());
+
+ mThreadedView = (CheckBoxPreference) findPreference(PREFERENCE_THREADED_VIEW);
+ mThreadedView.setChecked(K9.isThreadedViewEnabled());
+
if (K9.changeContactNameColor()) {
mChangeContactNameColor.setSummary(R.string.global_settings_registered_name_color_changed);
} else {
@@ -418,6 +427,7 @@ public class Prefs extends K9PreferenceActivity {
K9.setMessageListSenderAboveSubject(mSenderAboveSubject.isChecked());
K9.setShowContactName(mShowContactName.isChecked());
K9.setUseBackgroundAsUnreadIndicator(mBackgroundAsUnreadIndicator.isChecked());
+ K9.setThreadedViewEnabled(mThreadedView.isChecked());
K9.setChangeContactNameColor(mChangeContactNameColor.isChecked());
K9.setMessageViewFixedWidthFont(mFixedWidth.isChecked());
K9.setMessageViewReturnToList(mReturnToList.isChecked());
diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index d5860a4c3..d23d5799c 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -40,11 +40,9 @@ import com.fsck.k9.K9.Intents;
import com.fsck.k9.NotificationSetting;
import com.fsck.k9.Preferences;
import com.fsck.k9.R;
-import com.fsck.k9.SearchSpecification;
import com.fsck.k9.activity.FolderList;
import com.fsck.k9.activity.MessageList;
import com.fsck.k9.helper.NotificationBuilder;
-import com.fsck.k9.helper.Utility;
import com.fsck.k9.helper.power.TracingPowerManager;
import com.fsck.k9.helper.power.TracingPowerManager.TracingWakeLock;
import com.fsck.k9.mail.Address;
@@ -70,6 +68,8 @@ import com.fsck.k9.mail.store.LocalStore.LocalMessage;
import com.fsck.k9.mail.store.LocalStore.PendingCommand;
import com.fsck.k9.mail.store.UnavailableAccountException;
import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchSpecification;
/**
@@ -509,235 +509,39 @@ public class MessagingController implements Runnable {
});
}
-
-
- /**
- * List the messages in the local message store for the given folder asynchronously.
- *
- * @param account
- * @param folder
- * @param listener
- * @throws MessagingException
- */
- public void listLocalMessages(final Account account, final String folder, final MessagingListener listener) {
- threadPool.execute(new Runnable() {
- @Override
- public void run() {
- listLocalMessagesSynchronous(account, folder, listener);
- }
- });
- }
-
-
- /**
- * List the messages in the local message store for the given folder synchronously.
- *
- * @param account
- * @param folder
- * @param listener
- * @throws MessagingException
- */
- public void listLocalMessagesSynchronous(final Account account, final String folder, final MessagingListener listener) {
-
- for (MessagingListener l : getListeners(listener)) {
- l.listLocalMessagesStarted(account, folder);
- }
-
- LocalFolder localFolder = null;
- MessageRetrievalListener retrievalListener =
- new MessageRetrievalListener() {
- List pendingMessages = new ArrayList();
-
-
- @Override
- public void messageStarted(String message, int number, int ofTotal) {}
- @Override
- public void messageFinished(Message message, int number, int ofTotal) {
-
- if (!isMessageSuppressed(account, folder, message)) {
- pendingMessages.add(message);
- if (pendingMessages.size() > 10) {
- addPendingMessages();
- }
-
- } else {
- for (MessagingListener l : getListeners(listener)) {
- l.listLocalMessagesRemoveMessage(account, folder, message);
- }
- }
- }
- @Override
- public void messagesFinished(int number) {
- addPendingMessages();
- }
- private void addPendingMessages() {
- for (MessagingListener l : getListeners(listener)) {
- l.listLocalMessagesAddMessages(account, folder, pendingMessages);
- }
- pendingMessages.clear();
- }
- };
-
-
-
- try {
- LocalStore localStore = account.getLocalStore();
- localFolder = localStore.getFolder(folder);
- localFolder.open(OpenMode.READ_WRITE);
-
- //Purging followed by getting requires 2 DB queries.
- //TODO: Fix getMessages to allow auto-pruning at visible limit?
- localFolder.purgeToVisibleLimit(null);
- localFolder.getMessages(
- retrievalListener,
- false // Skip deleted messages
- );
- if (K9.DEBUG)
- Log.v(K9.LOG_TAG, "Got ack that callbackRunner finished");
-
- for (MessagingListener l : getListeners(listener)) {
- l.listLocalMessagesFinished(account, folder);
- }
- } catch (Exception e) {
- for (MessagingListener l : getListeners(listener)) {
- l.listLocalMessagesFailed(account, folder, e.getMessage());
- }
- addErrorMessage(account, null, e);
- } finally {
- closeFolder(localFolder);
- }
- }
-
- public void searchLocalMessages(SearchSpecification searchSpecification, final Message[] messages, final MessagingListener listener) {
- searchLocalMessages(searchSpecification.getAccountUuids(), searchSpecification.getFolderNames(), messages,
- searchSpecification.getQuery(), searchSpecification.isIntegrate(), searchSpecification.getRequiredFlags(), searchSpecification.getForbiddenFlags(), listener);
- }
-
-
/**
* Find all messages in any local account which match the query 'query'
* @throws MessagingException
*/
- public void searchLocalMessages(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate,
- final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) {
- if (K9.DEBUG) {
- Log.i(K9.LOG_TAG, "searchLocalMessages ("
- + "accountUuids=" + Utility.combine(accountUuids, ',')
- + ", folderNames = " + Utility.combine(folderNames, ',')
- + ", messages.size() = " + (messages != null ? messages.length : -1)
- + ", query = " + query
- + ", integrate = " + integrate
- + ", requiredFlags = " + Utility.combine(requiredFlags, ',')
- + ", forbiddenFlags = " + Utility.combine(forbiddenFlags, ',')
- + ")");
- }
-
+ public void searchLocalMessages(final LocalSearch search, final MessagingListener listener) {
threadPool.execute(new Runnable() {
@Override
public void run() {
- searchLocalMessagesSynchronous(accountUuids, folderNames, messages, query, integrate, requiredFlags, forbiddenFlags, listener);
+ searchLocalMessagesSynchronous(search, listener);
}
});
}
- public void searchLocalMessagesSynchronous(final String[] accountUuids, final String[] folderNames, final Message[] messages, final String query, final boolean integrate, final Flag[] requiredFlags, final Flag[] forbiddenFlags, final MessagingListener listener) {
+ public void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) {
final AccountStats stats = new AccountStats();
- final Set accountUuidsSet = new HashSet();
- if (accountUuids != null) {
- accountUuidsSet.addAll(Arrays.asList(accountUuids));
- }
- final Preferences prefs = Preferences.getPreferences(mApplication.getApplicationContext());
- List foldersToSearch = null;
- boolean displayableOnly = false;
- boolean noSpecialFolders = true;
- for (final Account account : prefs.getAvailableAccounts()) {
- if (accountUuids != null && !accountUuidsSet.contains(account.getUuid())) {
+ final HashSet uuidSet = new HashSet(Arrays.asList(search.getAccountUuids()));
+ Account[] accounts = Preferences.getPreferences(mApplication.getApplicationContext()).getAccounts();
+ boolean allAccounts = uuidSet.contains(SearchSpecification.ALL_ACCOUNTS);
+
+ // for every account we want to search do the query in the localstore
+ for (final Account account : accounts) {
+
+ if (!allAccounts && !uuidSet.contains(account.getUuid())) {
continue;
}
- if (accountUuids != null && accountUuidsSet.contains(account.getUuid())) {
- displayableOnly = true;
- noSpecialFolders = true;
- } else if (!integrate && folderNames == null) {
- Account.Searchable searchableFolders = account.getSearchableFolders();
- switch (searchableFolders) {
- case NONE:
- continue;
- case DISPLAYABLE:
- displayableOnly = true;
- break;
-
- }
- }
- List messagesToSearch = null;
- if (messages != null) {
- messagesToSearch = new LinkedList();
- for (Message message : messages) {
- if (message.getFolder().getAccount().getUuid().equals(account.getUuid())) {
- messagesToSearch.add(message);
- }
- }
- if (messagesToSearch.isEmpty()) {
- continue;
- }
- }
- if (listener != null) {
- listener.listLocalMessagesStarted(account, null);
- }
-
- if (integrate || displayableOnly || folderNames != null || noSpecialFolders) {
- List tmpFoldersToSearch = new LinkedList();
- try {
- LocalStore store = account.getLocalStore();
- List extends Folder > folders = store.getPersonalNamespaces(false);
- Set folderNameSet = null;
- if (folderNames != null) {
- folderNameSet = new HashSet();
- folderNameSet.addAll(Arrays.asList(folderNames));
- }
- for (Folder folder : folders) {
- LocalFolder localFolder = (LocalFolder)folder;
- boolean include = true;
- folder.refresh(prefs);
- String localFolderName = localFolder.getName();
- if (integrate) {
- include = localFolder.isIntegrate();
- } else {
- if (folderNameSet != null) {
- if (!folderNameSet.contains(localFolderName))
-
- {
- include = false;
- }
- }
- // Never exclude the INBOX (see issue 1817)
- else if (noSpecialFolders && !localFolderName.equalsIgnoreCase(account.getInboxFolderName()) &&
- !localFolderName.equals(account.getArchiveFolderName()) && account.isSpecialFolder(localFolderName)) {
- include = false;
- } else if (displayableOnly && modeMismatch(account.getFolderDisplayMode(), folder.getDisplayClass())) {
- include = false;
- }
- }
-
- if (include) {
- tmpFoldersToSearch.add(localFolder);
- }
- }
- if (tmpFoldersToSearch.size() < 1) {
- continue;
- }
- foldersToSearch = tmpFoldersToSearch;
- } catch (MessagingException me) {
- Log.e(K9.LOG_TAG, "Unable to restrict search folders in Account " + account.getDescription() + ", searching all", me);
- addErrorMessage(account, null, me);
- }
-
- }
-
+ // Collecting statistics of the search result
MessageRetrievalListener retrievalListener = new MessageRetrievalListener() {
@Override
public void messageStarted(String message, int number, int ofTotal) {}
@Override
+ public void messagesFinished(int number) {}
+ @Override
public void messageFinished(Message message, int number, int ofTotal) {
if (!isMessageSuppressed(message.getFolder().getAccount(), message.getFolder().getName(), message)) {
List messages = new ArrayList();
@@ -749,22 +553,18 @@ public class MessagingController implements Runnable {
listener.listLocalMessagesAddMessages(account, null, messages);
}
}
-
- }
- @Override
- public void messagesFinished(int number) {
-
}
};
- try {
- String[] queryFields = {"html_content", "subject", "sender_list"};
- LocalStore localStore = account.getLocalStore();
- localStore.searchForMessages(retrievalListener, queryFields
- , query, foldersToSearch,
- messagesToSearch == null ? null : messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY),
- requiredFlags, forbiddenFlags);
+ // alert everyone the search has started
+ if (listener != null) {
+ listener.listLocalMessagesStarted(account, null);
+ }
+ // build and do the query in the localstore
+ try {
+ LocalStore localStore = account.getLocalStore();
+ localStore.searchForMessages(retrievalListener, search);
} catch (Exception e) {
if (listener != null) {
listener.listLocalMessagesFailed(account, null, e.getMessage());
@@ -776,6 +576,8 @@ public class MessagingController implements Runnable {
}
}
}
+
+ // publish the total search statistics
if (listener != null) {
listener.searchStats(stats);
}
@@ -824,24 +626,28 @@ public class MessagingController implements Runnable {
}
List messages = remoteFolder.search(query, requiredFlags, forbiddenFlags);
- if (listener != null) {
- listener.remoteSearchServerQueryComplete(acct, folderName, messages.size());
- }
if (K9.DEBUG) {
Log.i("Remote Search", "Remote search got " + messages.size() + " results");
}
- Collections.sort(messages, new UidReverseComparator());
+ // There's no need to fetch messages already completely downloaded
+ List remoteMessages = localFolder.extractNewMessages(messages);
+ messages.clear();
-
- int resultLimit = acct.getRemoteSearchNumResults();
- if (resultLimit > 0 && messages.size() > resultLimit) {
- extraResults = messages.subList(resultLimit, messages.size());
- messages = messages.subList(0, resultLimit);
+ if (listener != null) {
+ listener.remoteSearchServerQueryComplete(acct, folderName, remoteMessages.size());
}
- loadSearchResultsSynchronous(messages, localFolder, remoteFolder, listener);
+ Collections.sort(remoteMessages, new UidReverseComparator());
+
+ int resultLimit = acct.getRemoteSearchNumResults();
+ if (resultLimit > 0 && remoteMessages.size() > resultLimit) {
+ extraResults = remoteMessages.subList(resultLimit, remoteMessages.size());
+ remoteMessages = remoteMessages.subList(0, resultLimit);
+ }
+
+ loadSearchResultsSynchronous(remoteMessages, localFolder, remoteFolder, listener);
} catch (Exception e) {
@@ -2753,20 +2559,39 @@ public class MessagingController implements Runnable {
processPendingCommands(account);
}
- public void setFlag(
- final Message[] messages,
- final Flag flag,
- final boolean newState) {
+ public void setFlag(final List messages, final Flag flag, final boolean newState) {
+
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
- final List messages) {
- setFlag(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), flag,
+ final List accountMessages) {
+
+ setFlag(account, folder.getName(), accountMessages.toArray(EMPTY_MESSAGE_ARRAY), flag,
newState);
}
-
});
+ }
+ public void setFlagForThreads(final List messages, final Flag flag,
+ final boolean newState) {
+
+ actOnMessages(messages, new MessageActor() {
+ @Override
+ public void act(final Account account, final Folder folder,
+ final List accountMessages) {
+
+ try {
+ List messagesInThreads = collectMessagesInThreads(account,
+ accountMessages);
+
+ setFlag(account, folder.getName(),
+ messagesInThreads.toArray(EMPTY_MESSAGE_ARRAY), flag, newState);
+
+ } catch (MessagingException e) {
+ addErrorMessage(account, "Something went wrong in setFlagForThreads()", e);
+ }
+ }
+ });
}
/**
@@ -3023,14 +2848,15 @@ public class MessagingController implements Runnable {
false, true)) {
if (account.isMarkMessageAsReadOnView() && !message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true);
- setFlag(new Message[] { message }, Flag.SEEN, true);
+ setFlag(Collections.singletonList((Message) message),
+ Flag.SEEN, true);
}
}
return;
}
if (!message.isSet(Flag.SEEN)) {
message.setFlag(Flag.SEEN, true);
- setFlag(new Message[] { message }, Flag.SEEN, true);
+ setFlag(Collections.singletonList((Message) message), Flag.SEEN, true);
}
for (MessagingListener l : getListeners(listener)) {
@@ -3247,8 +3073,11 @@ public class MessagingController implements Runnable {
builder.setContentTitle(mApplication.getString(R.string.notification_bg_send_title));
builder.setContentText(account.getDescription());
- Intent intent = MessageList.actionHandleFolderIntent(mApplication, account,
- account.getInboxFolderName());
+ LocalSearch search = new LocalSearch(account.getInboxFolderName());
+ search.addAllowedFolder(account.getInboxFolderName());
+ search.addAccountUuid(account.getUuid());
+ Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
+
PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
builder.setContentIntent(pi);
@@ -3330,8 +3159,11 @@ public class MessagingController implements Runnable {
mApplication.getString(R.string.notification_bg_title_separator) +
folder.getName());
- Intent intent = MessageList.actionHandleFolderIntent(mApplication, account,
- account.getInboxFolderName());
+ LocalSearch search = new LocalSearch(account.getInboxFolderName());
+ search.addAllowedFolder(account.getInboxFolderName());
+ search.addAccountUuid(account.getUuid());
+ Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
+
PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
builder.setContentIntent(pi);
@@ -3597,40 +3429,90 @@ public class MessagingController implements Runnable {
return false;
}
}
- public void moveMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder,
- final MessagingListener listener) {
+ public void moveMessages(final Account account, final String srcFolder,
+ final List messages, final String destFolder,
+ final MessagingListener listener) {
+
for (Message message : messages) {
suppressMessage(account, srcFolder, message);
}
+
putBackground("moveMessages", null, new Runnable() {
@Override
public void run() {
- moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false, listener);
+ moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, false,
+ listener);
}
});
}
- public void moveMessage(final Account account, final String srcFolder, final Message message, final String destFolder,
- final MessagingListener listener) {
- moveMessages(account, srcFolder, new Message[] { message }, destFolder, listener);
+ public void moveMessagesInThread(final Account account, final String srcFolder,
+ final List messages, final String destFolder) {
+
+ for (Message message : messages) {
+ suppressMessage(account, srcFolder, message);
+ }
+
+ putBackground("moveMessagesInThread", null, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ List messagesInThreads = collectMessagesInThreads(account, messages);
+ moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder,
+ false, null);
+ } catch (MessagingException e) {
+ addErrorMessage(account, "Exception while moving messages", e);
+ }
+ }
+ });
}
- public void copyMessages(final Account account, final String srcFolder, final Message[] messages, final String destFolder,
- final MessagingListener listener) {
+ public void moveMessage(final Account account, final String srcFolder, final Message message,
+ final String destFolder, final MessagingListener listener) {
+
+ moveMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener);
+ }
+
+ public void copyMessages(final Account account, final String srcFolder,
+ final List messages, final String destFolder,
+ final MessagingListener listener) {
+
putBackground("copyMessages", null, new Runnable() {
@Override
public void run() {
- moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true, listener);
+ moveOrCopyMessageSynchronous(account, srcFolder, messages, destFolder, true,
+ listener);
}
});
}
- public void copyMessage(final Account account, final String srcFolder, final Message message, final String destFolder,
- final MessagingListener listener) {
- copyMessages(account, srcFolder, new Message[] { message }, destFolder, listener);
+
+ public void copyMessagesInThread(final Account account, final String srcFolder,
+ final List messages, final String destFolder) {
+
+ putBackground("copyMessagesInThread", null, new Runnable() {
+ @Override
+ public void run() {
+ try {
+ List messagesInThreads = collectMessagesInThreads(account, messages);
+ moveOrCopyMessageSynchronous(account, srcFolder, messagesInThreads, destFolder,
+ true, null);
+ } catch (MessagingException e) {
+ addErrorMessage(account, "Exception while copying messages", e);
+ }
+ }
+ });
}
- private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder, final Message[] inMessages,
- final String destFolder, final boolean isCopy, MessagingListener listener) {
+ public void copyMessage(final Account account, final String srcFolder, final Message message,
+ final String destFolder, final MessagingListener listener) {
+
+ copyMessages(account, srcFolder, Collections.singletonList(message), destFolder, listener);
+ }
+
+ private void moveOrCopyMessageSynchronous(final Account account, final String srcFolder,
+ final List inMessages, final String destFolder, final boolean isCopy,
+ MessagingListener listener) {
+
try {
Map uidMap = new HashMap();
Store localStore = account.getLocalStore();
@@ -3741,7 +3623,7 @@ public class MessagingController implements Runnable {
if (uid != null) {
Message message = localFolder.getMessage(uid);
if (message != null) {
- deleteMessages(new Message[] { message }, null);
+ deleteMessages(Collections.singletonList(message), null);
}
}
} catch (MessagingException me) {
@@ -3751,20 +3633,72 @@ public class MessagingController implements Runnable {
}
}
- public void deleteMessages(final Message[] messages, final MessagingListener listener) {
+ public void deleteThreads(final List messages) {
actOnMessages(messages, new MessageActor() {
@Override
public void act(final Account account, final Folder folder,
- final List messages) {
- for (Message message : messages) {
+ final List accountMessages) {
+
+ for (Message message : accountMessages) {
+ suppressMessage(account, folder.getName(), message);
+ }
+
+ putBackground("deleteThreads", null, new Runnable() {
+ @Override
+ public void run() {
+ deleteThreadsSynchronous(account, folder.getName(), accountMessages);
+ }
+ });
+ }
+ });
+ }
+
+ public void deleteThreadsSynchronous(Account account, String folderName,
+ List messages) {
+
+ try {
+ List messagesToDelete = collectMessagesInThreads(account, messages);
+
+ deleteMessagesSynchronous(account, folderName,
+ messagesToDelete.toArray(EMPTY_MESSAGE_ARRAY), null);
+ } catch (MessagingException e) {
+ Log.e(K9.LOG_TAG, "Something went wrong while deleting threads", e);
+ }
+ }
+
+ public List collectMessagesInThreads(Account account, List messages)
+ throws MessagingException {
+
+ LocalStore localStore = account.getLocalStore();
+
+ List messagesInThreads = new ArrayList();
+ for (Message message : messages) {
+ long rootId = ((LocalMessage) message).getRootId();
+ long threadId = (rootId == -1) ? message.getId() : rootId;
+
+ Message[] messagesInThread = localStore.getMessagesInThread(threadId);
+ Collections.addAll(messagesInThreads, messagesInThread);
+ }
+
+ return messagesInThreads;
+ }
+
+ public void deleteMessages(final List messages, final MessagingListener listener) {
+ actOnMessages(messages, new MessageActor() {
+
+ @Override
+ public void act(final Account account, final Folder folder,
+ final List accountMessages) {
+ for (Message message : accountMessages) {
suppressMessage(account, folder.getName(), message);
}
putBackground("deleteMessages", null, new Runnable() {
@Override
public void run() {
- deleteMessagesSynchronous(account, folder.getName(), messages.toArray(EMPTY_MESSAGE_ARRAY), listener);
+ deleteMessagesSynchronous(account, folder.getName(),
+ accountMessages.toArray(EMPTY_MESSAGE_ARRAY), listener);
}
});
}
@@ -5024,7 +4958,7 @@ public class MessagingController implements Runnable {
}
- private void actOnMessages(Message[] messages, MessageActor actor) {
+ private void actOnMessages(List messages, MessageActor actor) {
Map>> accountMap = new HashMap>>();
for (Message message : messages) {
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index d87b8356e..d5c38e193 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -4,30 +4,39 @@ import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Date;
import java.util.EnumMap;
-import java.util.Iterator;
+import java.util.HashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
import java.util.concurrent.Future;
import android.app.Activity;
-import android.app.SearchManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences.Editor;
+import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
+import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.DialogFragment;
+import android.support.v4.app.LoaderManager;
+import android.support.v4.app.LoaderManager.LoaderCallbacks;
+import android.support.v4.content.CursorLoader;
+import android.support.v4.content.Loader;
+import android.support.v4.widget.CursorAdapter;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.style.AbsoluteSizeSpan;
import android.text.style.ForegroundColorSpan;
-import android.text.style.StyleSpan;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
@@ -39,7 +48,6 @@ import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.AdapterView.OnItemClickListener;
-import android.widget.BaseAdapter;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
@@ -64,75 +72,85 @@ import com.fsck.k9.R;
import com.fsck.k9.activity.ActivityListener;
import com.fsck.k9.activity.ChooseFolder;
import com.fsck.k9.activity.FolderInfoHolder;
-import com.fsck.k9.activity.MessageInfoHolder;
import com.fsck.k9.activity.MessageReference;
import com.fsck.k9.controller.MessagingController;
-import com.fsck.k9.controller.MessagingListener;
+import com.fsck.k9.fragment.ConfirmationDialogFragment;
import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmentListener;
import com.fsck.k9.helper.MessageHelper;
+import com.fsck.k9.helper.MergeCursorWithUniqueId;
+import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
-import com.fsck.k9.mail.Store;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Folder.OpenMode;
import com.fsck.k9.mail.store.LocalStore;
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
+import com.fsck.k9.provider.EmailProvider;
+import com.fsck.k9.provider.EmailProvider.MessageColumns;
+import com.fsck.k9.provider.EmailProvider.SpecialColumns;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchSpecification;
+import com.fsck.k9.search.SqlQueryBuilder;
import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView;
public class MessageListFragment extends SherlockFragment implements OnItemClickListener,
- ConfirmationDialogFragmentListener {
+ ConfirmationDialogFragmentListener, LoaderCallbacks {
- public static MessageListFragment newInstance(Account account, String folderName) {
+ private static final String[] THREADED_PROJECTION = {
+ MessageColumns.ID,
+ MessageColumns.UID,
+ MessageColumns.INTERNAL_DATE,
+ MessageColumns.SUBJECT,
+ MessageColumns.DATE,
+ MessageColumns.SENDER_LIST,
+ MessageColumns.TO_LIST,
+ MessageColumns.CC_LIST,
+ MessageColumns.FLAGS,
+ MessageColumns.ATTACHMENT_COUNT,
+ MessageColumns.FOLDER_ID,
+ MessageColumns.PREVIEW,
+ MessageColumns.THREAD_ROOT,
+ SpecialColumns.ACCOUNT_UUID,
+ SpecialColumns.FOLDER_NAME,
+
+ MessageColumns.THREAD_COUNT,
+ };
+
+ private static final int ID_COLUMN = 0;
+ private static final int UID_COLUMN = 1;
+ private static final int INTERNAL_DATE_COLUMN = 2;
+ private static final int SUBJECT_COLUMN = 3;
+ private static final int DATE_COLUMN = 4;
+ private static final int SENDER_LIST_COLUMN = 5;
+ private static final int TO_LIST_COLUMN = 6;
+ private static final int CC_LIST_COLUMN = 7;
+ private static final int FLAGS_COLUMN = 8;
+ private static final int ATTACHMENT_COUNT_COLUMN = 9;
+ private static final int FOLDER_ID_COLUMN = 10;
+ private static final int PREVIEW_COLUMN = 11;
+ private static final int THREAD_ROOT_COLUMN = 12;
+ private static final int ACCOUNT_UUID_COLUMN = 13;
+ private static final int FOLDER_NAME_COLUMN = 14;
+ private static final int THREAD_COUNT_COLUMN = 15;
+
+ private static final String[] PROJECTION = Utility.copyOf(THREADED_PROJECTION,
+ THREAD_COUNT_COLUMN);
+
+
+ public static MessageListFragment newInstance(LocalSearch search, boolean threadedList) {
MessageListFragment fragment = new MessageListFragment();
-
Bundle args = new Bundle();
- args.putString(ARG_ACCOUNT, account.getUuid());
- args.putString(ARG_FOLDER, folderName);
+ args.putParcelable(ARG_SEARCH, search);
+ args.putBoolean(ARG_THREADED_LIST, threadedList);
fragment.setArguments(args);
-
return fragment;
}
- public static MessageListFragment newInstance(String title, String[] accountUuids,
- String[] folderNames, String queryString, Flag[] flags,
- Flag[] forbiddenFlags, boolean integrate) {
-
- MessageListFragment fragment = new MessageListFragment();
-
- Bundle args = new Bundle();
- args.putStringArray(ARG_ACCOUNT_UUIDS, accountUuids);
- args.putStringArray(ARG_FOLDER_NAMES, folderNames);
- args.putString(ARG_QUERY, queryString);
- if (flags != null) {
- args.putString(ARG_QUERY_FLAGS, Utility.combine(flags, ','));
- }
- if (forbiddenFlags != null) {
- args.putString(ARG_FORBIDDEN_FLAGS, Utility.combine(forbiddenFlags, ','));
- }
- args.putBoolean(ARG_INTEGRATE, integrate);
- args.putString(ARG_TITLE, title);
- fragment.setArguments(args);
-
- return fragment;
- }
-
- public static MessageListFragment newInstance(String searchAccount, String searchFolder,
- String queryString, boolean remoteSearch) {
- MessageListFragment fragment = new MessageListFragment();
-
- Bundle args = new Bundle();
- args.putString(ARG_SEARCH_ACCOUNT, searchAccount);
- args.putString(ARG_SEARCH_FOLDER, searchFolder);
- args.putString(ARG_QUERY, queryString);
- args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
- fragment.setArguments(args);
-
- return fragment;
- }
-
-
/**
* Reverses the result of a {@link Comparator}.
*
@@ -143,7 +161,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
/**
* @param delegate
- * Never null
.
+ * Never {@code null}.
*/
public ReverseComparator(final Comparator delegate) {
mDelegate = delegate;
@@ -154,7 +172,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
// arg1 & 2 are mixed up, this is done on purpose
return mDelegate.compare(object2, object1);
}
-
}
/**
@@ -163,12 +180,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
* @param
*/
public static class ComparatorChain implements Comparator {
-
private List> mChain;
/**
* @param chain
- * Comparator chain. Never null
.
+ * Comparator chain. Never {@code null}.
*/
public ComparatorChain(final List> chain) {
mChain = chain;
@@ -185,129 +201,117 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
return result;
}
-
}
- public static class AttachmentComparator implements Comparator {
+ public static class ReverseIdComparator implements Comparator {
+ private int mIdColumn = -1;
@Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- return (object1.message.hasAttachments() ? 0 : 1) - (object2.message.hasAttachments() ? 0 : 1);
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ if (mIdColumn == -1) {
+ mIdColumn = cursor1.getColumnIndex("_id");
+ }
+ long o1Id = cursor1.getLong(mIdColumn);
+ long o2Id = cursor2.getLong(mIdColumn);
+ return (o1Id > o2Id) ? -1 : 1;
}
-
}
- public static class FlaggedComparator implements Comparator {
+ public static class AttachmentComparator implements Comparator {
@Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- return (object1.flagged ? 0 : 1) - (object2.flagged ? 0 : 1);
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ int o1HasAttachment = (cursor1.getInt(ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1;
+ int o2HasAttachment = (cursor2.getInt(ATTACHMENT_COUNT_COLUMN) > 0) ? 0 : 1;
+ return o1HasAttachment - o2HasAttachment;
}
-
}
- public static class UnreadComparator implements Comparator {
+ public static class FlaggedComparator implements Comparator {
@Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- return (object1.read ? 1 : 0) - (object2.read ? 1 : 0);
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ int o1IsFlagged = (cursor1.getString(FLAGS_COLUMN).contains("FLAGGED")) ? 0 : 1;
+ int o2IsFlagged = (cursor2.getString(FLAGS_COLUMN).contains("FLAGGED")) ? 0 : 1;
+ return o1IsFlagged - o2IsFlagged;
}
-
}
- public static class SenderComparator implements Comparator {
+ public static class UnreadComparator implements Comparator {
@Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- if (object1.compareCounterparty == null) {
- return (object2.compareCounterparty == null ? 0 : 1);
- } else if (object2.compareCounterparty == null) {
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ int o1IsUnread = (cursor1.getString(FLAGS_COLUMN).contains("SEEN")) ? 1 : 0;
+ int o2IsUnread = (cursor2.getString(FLAGS_COLUMN).contains("SEEN")) ? 1 : 0;
+ return o1IsUnread - o2IsUnread;
+ }
+ }
+
+ public static class DateComparator implements Comparator {
+
+ @Override
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ long o1Date = cursor1.getLong(DATE_COLUMN);
+ long o2Date = cursor2.getLong(DATE_COLUMN);
+ if (o1Date < o2Date) {
+ return -1;
+ } else if (o1Date == o2Date) {
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+ }
+
+ public static class ArrivalComparator implements Comparator {
+
+ @Override
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ long o1Date = cursor1.getLong(INTERNAL_DATE_COLUMN);
+ long o2Date = cursor2.getLong(INTERNAL_DATE_COLUMN);
+ if (o1Date == o2Date) {
+ return 0;
+ } else if (o1Date < o2Date) {
return -1;
} else {
- return object1.compareCounterparty.toLowerCase().compareTo(object2.compareCounterparty.toLowerCase());
+ return 1;
}
}
-
}
- public static class DateComparator implements Comparator {
+ public static class SubjectComparator implements Comparator {
@Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- if (object1.compareDate == null) {
- return (object2.compareDate == null ? 0 : 1);
- } else if (object2.compareDate == null) {
- return -1;
- } else {
- return object1.compareDate.compareTo(object2.compareDate);
- }
+ public int compare(Cursor cursor1, Cursor cursor2) {
+ String subject1 = cursor1.getString(SUBJECT_COLUMN);
+ String subject2 = cursor2.getString(SUBJECT_COLUMN);
+
+ return subject1.compareToIgnoreCase(subject2);
}
-
}
- public static class ArrivalComparator implements Comparator {
-
- @Override
- public int compare(MessageInfoHolder object1, MessageInfoHolder object2) {
- return object1.compareArrival.compareTo(object2.compareArrival);
- }
-
- }
-
- public static class SubjectComparator implements Comparator {
-
- @Override
- public int compare(MessageInfoHolder arg0, MessageInfoHolder arg1) {
- // XXX doesn't respect the Comparator contract since it alters the compared object
- if (arg0.compareSubject == null) {
- arg0.compareSubject = Utility.stripSubject(arg0.message.getSubject());
- }
- if (arg1.compareSubject == null) {
- arg1.compareSubject = Utility.stripSubject(arg1.message.getSubject());
- }
- return arg0.compareSubject.compareToIgnoreCase(arg1.compareSubject);
- }
-
- }
-
- /**
- * Immutable empty {@link Message} array
- */
- private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0];
-
private static final int ACTIVITY_CHOOSE_FOLDER_MOVE = 1;
private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
- private static final String ARG_ACCOUNT = "account";
- private static final String ARG_FOLDER = "folder";
- private static final String ARG_REMOTE_SEARCH = "remote_search";
- private static final String ARG_QUERY = "query";
- private static final String ARG_SEARCH_ACCOUNT = "search_account";
- private static final String ARG_SEARCH_FOLDER = "search_folder";
- private static final String ARG_QUERY_FLAGS = "queryFlags";
- private static final String ARG_FORBIDDEN_FLAGS = "forbiddenFlags";
- private static final String ARG_INTEGRATE = "integrate";
- private static final String ARG_ACCOUNT_UUIDS = "accountUuids";
- private static final String ARG_FOLDER_NAMES = "folderNames";
- private static final String ARG_TITLE = "title";
-
+ private static final String ARG_SEARCH = "searchObject";
+ private static final String ARG_THREADED_LIST = "threadedList";
private static final String STATE_LIST_POSITION = "listPosition";
/**
* Maps a {@link SortType} to a {@link Comparator} implementation.
*/
- private static final Map> SORT_COMPARATORS;
+ private static final Map> SORT_COMPARATORS;
static {
// fill the mapping at class time loading
- final Map> map = new EnumMap>(SortType.class);
+ final Map> map =
+ new EnumMap>(SortType.class);
map.put(SortType.SORT_ATTACHMENT, new AttachmentComparator());
map.put(SortType.SORT_DATE, new DateComparator());
map.put(SortType.SORT_ARRIVAL, new ArrivalComparator());
map.put(SortType.SORT_FLAGGED, new FlaggedComparator());
- map.put(SortType.SORT_SENDER, new SenderComparator());
map.put(SortType.SORT_SUBJECT, new SubjectComparator());
map.put(SortType.SORT_UNREAD, new UnreadComparator());
@@ -331,28 +335,25 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private MessagingController mController;
private Account mAccount;
+ private String[] mAccountUuids;
private int mUnreadMessageCount = 0;
+ private Cursor[] mCursors;
+ private int mUniqueIdColumn;
+
/**
- * Stores the name of the folder that we want to open as soon as possible
- * after load.
+ * Stores the name of the folder that we want to open as soon as possible after load.
*/
private String mFolderName;
- /**
- * If we're doing a search, this contains the query string.
- */
- private String mQueryString;
- private Flag[] mQueryFlags = null;
- private Flag[] mForbiddenFlags = null;
- private boolean mRemoteSearch = false;
- private String mSearchAccount = null;
- private String mSearchFolder = null;
+ private boolean mRemoteSearchPerformed = false;
private Future mRemoteSearchFuture = null;
- private boolean mIntegrate = false;
- private String[] mAccountUuids = null;
- private String[] mFolderNames = null;
+ public List mExtraSearchResults;
+
private String mTitle;
+ private LocalSearch mSearch = null;
+ private boolean mSingleAccountMode;
+ private boolean mSingleFolderMode;
private MessageListHandler mHandler = new MessageListHandler();
@@ -363,6 +364,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private boolean mCheckboxes = true;
private int mSelectedCount = 0;
+ private Set mSelected = new HashSet();
private FontSizes mFontSizes = K9.getFontSizes();
@@ -374,11 +376,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private Boolean mHasConnectivity;
/**
- * Relevant messages for the current context when we have to remember the
- * chosen messages between user interactions (eg. Selecting a folder for
- * move operation)
+ * Relevant messages for the current context when we have to remember the chosen messages
+ * between user interactions (e.g. selecting a folder for move operation).
*/
- private List mActiveMessages;
+ private List mActiveMessages;
/* package visibility for faster inner class access */
MessageHelper mMessageHelper;
@@ -391,6 +392,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private DateFormat mTimeFormat;
+ private boolean mThreadedList;
+
+
+ private Context mContext;
+
+ private final ActivityListener mListener = new MessageListActivityListener();
+
+ private Preferences mPreferences;
/**
* This class is used to run operations that modify UI elements in the UI thread.
@@ -402,25 +411,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
* perform the operation in the calling thread.
*/
class MessageListHandler extends Handler {
- private static final int ACTION_REMOVE_MESSAGE = 1;
- private static final int ACTION_RESET_UNREAD_COUNT = 2;
- private static final int ACTION_SORT_MESSAGES = 3;
- private static final int ACTION_FOLDER_LOADING = 4;
- private static final int ACTION_REFRESH_TITLE = 5;
- private static final int ACTION_PROGRESS = 6;
+ private static final int ACTION_FOLDER_LOADING = 1;
+ private static final int ACTION_REFRESH_TITLE = 2;
+ private static final int ACTION_PROGRESS = 3;
+ private static final int ACTION_REMOTE_SEARCH_FINISHED = 4;
- public void removeMessage(MessageReference messageReference) {
- android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOVE_MESSAGE,
- messageReference);
- sendMessage(msg);
- }
-
- public void sortMessages() {
- android.os.Message msg = android.os.Message.obtain(this, ACTION_SORT_MESSAGES);
- sendMessage(msg);
- }
-
public void folderLoading(String folder, boolean loading) {
android.os.Message msg = android.os.Message.obtain(this, ACTION_FOLDER_LOADING,
(loading) ? 1 : 0, 0, folder);
@@ -438,6 +434,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
sendMessage(msg);
}
+ public void remoteSearchFinished() {
+ android.os.Message msg = android.os.Message.obtain(this, ACTION_REMOTE_SEARCH_FINISHED);
+ sendMessage(msg);
+ }
+
public void updateFooter(final String message, final boolean showProgress) {
post(new Runnable() {
@Override
@@ -447,47 +448,23 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
});
}
- public void changeMessageUid(final MessageReference ref, final String newUid) {
- // Instead of explicitly creating a container to be able to pass both arguments in a
- // Message we post a Runnable to the message queue.
- post(new Runnable() {
- @Override
- public void run() {
- mAdapter.changeMessageUid(ref, newUid);
- }
- });
- }
-
- public void addOrUpdateMessages(final Account account, final String folderName,
- final List providedMessages, final boolean verifyAgainstSearch) {
- // We copy the message list because it's later modified by MessagingController
- final List messages = new ArrayList(providedMessages);
-
- post(new Runnable() {
- @Override
- public void run() {
- mAdapter.addOrUpdateMessages(account, folderName, messages,
- verifyAgainstSearch);
- }
- });
- }
-
@Override
public void handleMessage(android.os.Message msg) {
+ // The following messages don't need an attached activity.
switch (msg.what) {
- case ACTION_REMOVE_MESSAGE: {
- MessageReference messageReference = (MessageReference) msg.obj;
- mAdapter.removeMessage(messageReference);
- break;
- }
- case ACTION_RESET_UNREAD_COUNT: {
- mAdapter.resetUnreadCount();
- break;
- }
- case ACTION_SORT_MESSAGES: {
- mAdapter.sortMessages();
- break;
+ case ACTION_REMOTE_SEARCH_FINISHED: {
+ MessageListFragment.this.remoteSearchFinished();
+ return;
}
+ }
+
+ // Discard messages if the fragment isn't attached to an activity anymore.
+ Activity activity = getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ switch (msg.what) {
case ACTION_FOLDER_LOADING: {
String folder = (String) msg.obj;
boolean loading = (msg.arg1 == 1);
@@ -509,37 +486,35 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
/**
* @return The comparator to use to display messages in an ordered
- * fashion. Never null
.
+ * fashion. Never {@code null}.
*/
- protected Comparator getComparator() {
- final List> chain = new ArrayList>(2 /* we add 2 comparators at most */);
+ protected Comparator getComparator() {
+ final List> chain =
+ new ArrayList>(3 /* we add 3 comparators at most */);
- {
- // add the specified comparator
- final Comparator comparator = SORT_COMPARATORS.get(mSortType);
- if (mSortAscending) {
- chain.add(comparator);
+ // Add the specified comparator
+ final Comparator comparator = SORT_COMPARATORS.get(mSortType);
+ if (mSortAscending) {
+ chain.add(comparator);
+ } else {
+ chain.add(new ReverseComparator(comparator));
+ }
+
+ // Add the date comparator if not already specified
+ if (mSortType != SortType.SORT_DATE && mSortType != SortType.SORT_ARRIVAL) {
+ final Comparator dateComparator = SORT_COMPARATORS.get(SortType.SORT_DATE);
+ if (mSortDateAscending) {
+ chain.add(dateComparator);
} else {
- chain.add(new ReverseComparator(comparator));
+ chain.add(new ReverseComparator(dateComparator));
}
}
- {
- // add the date comparator if not already specified
- if (mSortType != SortType.SORT_DATE && mSortType != SortType.SORT_ARRIVAL) {
- final Comparator comparator = SORT_COMPARATORS.get(SortType.SORT_DATE);
- if (mSortDateAscending) {
- chain.add(comparator);
- } else {
- chain.add(new ReverseComparator(comparator));
- }
- }
- }
+ // Add the id comparator
+ chain.add(new ReverseIdComparator());
- // build the comparator chain
- final Comparator chainComparator = new ComparatorChain(chain);
-
- return chainComparator;
+ // Build the comparator chain
+ return new ComparatorChain(chain);
}
private void folderLoading(String folder, boolean loading) {
@@ -551,7 +526,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private void refreshTitle() {
setWindowTitle();
- if (!mRemoteSearch) {
+ if (!mSearch.isManualSearch()) {
setWindowProgress();
}
}
@@ -559,10 +534,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private void setWindowProgress() {
int level = Window.PROGRESS_END;
- if (mCurrentFolder != null && mCurrentFolder.loading && mAdapter.mListener.getFolderTotal() > 0) {
- int divisor = mAdapter.mListener.getFolderTotal();
+ if (mCurrentFolder != null && mCurrentFolder.loading && mListener.getFolderTotal() > 0) {
+ int divisor = mListener.getFolderTotal();
if (divisor != 0) {
- level = (Window.PROGRESS_END / divisor) * (mAdapter.mListener.getFolderCompleted()) ;
+ level = (Window.PROGRESS_END / divisor) * (mListener.getFolderCompleted()) ;
if (level > Window.PROGRESS_END) {
level = Window.PROGRESS_END;
}
@@ -574,20 +549,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private void setWindowTitle() {
// regular folder content display
- if (mFolderName != null) {
+ if (!isManualSearch() && mSingleFolderMode) {
Activity activity = getActivity();
String displayName = FolderInfoHolder.getDisplayName(activity, mAccount,
mFolderName);
mFragmentListener.setMessageListTitle(displayName);
- String operation = mAdapter.mListener.getOperation(activity, getTimeFormat()).trim();
+ String operation = mListener.getOperation(activity, getTimeFormat()).trim();
if (operation.length() < 1) {
mFragmentListener.setMessageListSubTitle(mAccount.getEmail());
} else {
mFragmentListener.setMessageListSubTitle(operation);
}
- } else if (mQueryString != null) {
+ } else {
// query result display. This may be for a search folder as opposed to a user-initiated search.
if (mTitle != null) {
// This was a search folder; the search folder has overridden our title.
@@ -604,8 +579,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
if (mUnreadMessageCount == 0) {
mFragmentListener.setUnreadCount(0);
} else {
- if (mQueryString != null && mTitle == null) {
- // This is a search result. The unread message count is easily confused
+ if (!mSingleFolderMode && mTitle == null) {
+ // The unread message count is easily confused
// with total number of messages in the search result, so let's hide it.
mFragmentListener.setUnreadCount(0);
} else {
@@ -615,7 +590,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
private void setupFormats() {
- mTimeFormat = android.text.format.DateFormat.getTimeFormat(getActivity());
+ mTimeFormat = android.text.format.DateFormat.getTimeFormat(mContext);
}
private DateFormat getTimeFormat() {
@@ -641,35 +616,52 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
@Override
public void onItemClick(AdapterView> parent, View view, int position, long id) {
if (view == mFooterView) {
- if (mCurrentFolder != null && !mRemoteSearch) {
- mController.loadMoreMessages(mAccount, mFolderName, mAdapter.mListener);
- } else if (mRemoteSearch && mAdapter.mExtraSearchResults != null && mAdapter.mExtraSearchResults.size() > 0 && mSearchAccount != null) {
- int numResults = mAdapter.mExtraSearchResults.size();
- Context appContext = getActivity().getApplicationContext();
- Account account = Preferences.getPreferences(appContext).getAccount(mSearchAccount);
- if (account == null) {
- mHandler.updateFooter("", false);
- return;
- }
- int limit = account.getRemoteSearchNumResults();
- List toProcess = mAdapter.mExtraSearchResults;
+ if (mCurrentFolder != null && !mSearch.isManualSearch()) {
+
+ mController.loadMoreMessages(mAccount, mFolderName, null);
+
+ } else if (mCurrentFolder != null && isRemoteSearch() &&
+ mExtraSearchResults != null && mExtraSearchResults.size() > 0) {
+
+ int numResults = mExtraSearchResults.size();
+ int limit = mAccount.getRemoteSearchNumResults();
+
+ List toProcess = mExtraSearchResults;
+
if (limit > 0 && numResults > limit) {
toProcess = toProcess.subList(0, limit);
- mAdapter.mExtraSearchResults = mAdapter.mExtraSearchResults.subList(limit, mAdapter.mExtraSearchResults.size());
+ mExtraSearchResults = mExtraSearchResults.subList(limit,
+ mExtraSearchResults.size());
} else {
- mAdapter.mExtraSearchResults = null;
- mHandler.updateFooter("", false);
+ mExtraSearchResults = null;
+ updateFooter("", false);
}
- mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener);
+
+ mController.loadSearchResults(mAccount, mCurrentFolder.name, toProcess, mListener);
}
+
return;
}
- final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position);
+ Cursor cursor = (Cursor) parent.getItemAtPosition(position);
if (mSelectedCount > 0) {
- toggleMessageSelect(message);
+ toggleMessageSelect(position);
} else {
- onOpenMessage(message);
+ Account account = getAccountFromCursor(cursor);
+
+ long folderId = cursor.getLong(FOLDER_ID_COLUMN);
+ String folderName = getFolderNameById(account, folderId);
+
+ if (mThreadedList && cursor.getInt(THREAD_COUNT_COLUMN) > 1) {
+ long rootId = cursor.getLong(THREAD_ROOT_COLUMN);
+ mFragmentListener.showThread(account, folderName, rootId);
+ } else {
+ MessageReference ref = new MessageReference();
+ ref.accountUuid = account.getUuid();
+ ref.folderName = folderName;
+ ref.uid = cursor.getString(UID_COLUMN);
+ onOpenMessage(ref);
+ }
}
}
@@ -677,6 +669,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public void onAttach(Activity activity) {
super.onAttach(activity);
+ mContext = activity.getApplicationContext();
+
try {
mFragmentListener = (MessageListFragmentListener) activity;
} catch (ClassCastException e) {
@@ -689,6 +683,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mPreferences = Preferences.getPreferences(getActivity().getApplicationContext());
mController = MessagingController.getInstance(getActivity().getApplication());
mPreviewLines = K9.messageListPreviewLines();
@@ -722,64 +717,131 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mMessageHelper = MessageHelper.getInstance(getActivity());
initializeMessageList();
+
+ // This needs to be done before initializing the cursor loader below
+ initializeSortSettings();
+
+ LoaderManager loaderManager = getLoaderManager();
+ int len = mAccountUuids.length;
+ mCursors = new Cursor[len];
+ for (int i = 0; i < len; i++) {
+ loaderManager.initLoader(i, null, this);
+ }
+ }
+
+ private void initializeSortSettings() {
+ if (mSingleAccountMode) {
+ mSortType = mAccount.getSortType();
+ mSortAscending = mAccount.isSortAscending(mSortType);
+ mSortDateAscending = mAccount.isSortAscending(SortType.SORT_DATE);
+ } else {
+ mSortType = K9.getSortType();
+ mSortAscending = K9.isSortAscending(mSortType);
+ mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
+ }
}
private void decodeArguments() {
Bundle args = getArguments();
- mQueryString = args.getString(SearchManager.QUERY);
- mFolderName = args.getString(ARG_FOLDER);
- mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false);
- mSearchAccount = args.getString(ARG_SEARCH_ACCOUNT);
- mSearchFolder = args.getString(ARG_SEARCH_FOLDER);
+ mThreadedList = args.getBoolean(ARG_THREADED_LIST, false);
+ mSearch = args.getParcelable(ARG_SEARCH);
+ mTitle = mSearch.getName();
- String accountUuid = args.getString(ARG_ACCOUNT);
+ String[] accountUuids = mSearch.getAccountUuids();
- Context appContext = getActivity().getApplicationContext();
- mAccount = Preferences.getPreferences(appContext).getAccount(accountUuid);
-
- String queryFlags = args.getString(ARG_QUERY_FLAGS);
- if (queryFlags != null) {
- String[] flagStrings = queryFlags.split(",");
- mQueryFlags = new Flag[flagStrings.length];
- for (int i = 0; i < flagStrings.length; i++) {
- mQueryFlags[i] = Flag.valueOf(flagStrings[i]);
- }
+ mSingleAccountMode = false;
+ if (accountUuids.length == 1 && !mSearch.searchAllAccounts()) {
+ mSingleAccountMode = true;
+ mAccount = mPreferences.getAccount(accountUuids[0]);
}
- String forbiddenFlags = args.getString(ARG_FORBIDDEN_FLAGS);
- if (forbiddenFlags != null) {
- String[] flagStrings = forbiddenFlags.split(",");
- mForbiddenFlags = new Flag[flagStrings.length];
- for (int i = 0; i < flagStrings.length; i++) {
- mForbiddenFlags[i] = Flag.valueOf(flagStrings[i]);
- }
+ mSingleFolderMode = false;
+ if (mSingleAccountMode && (mSearch.getFolderNames().size() == 1)) {
+ mSingleFolderMode = true;
+ mFolderName = mSearch.getFolderNames().get(0);
+ mCurrentFolder = getFolder(mFolderName, mAccount);
}
- mIntegrate = args.getBoolean(ARG_INTEGRATE, false);
- mAccountUuids = args.getStringArray(ARG_ACCOUNT_UUIDS);
- mFolderNames = args.getStringArray(ARG_FOLDER_NAMES);
- mTitle = args.getString(ARG_TITLE);
+ if (mSingleAccountMode) {
+ mAccountUuids = new String[] { mAccount.getUuid() };
+ } else {
+ if (accountUuids.length == 1 &&
+ accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
+
+ Account[] accounts = mPreferences.getAccounts();
+
+ mAccountUuids = new String[accounts.length];
+ for (int i = 0, len = accounts.length; i < len; i++) {
+ mAccountUuids[i] = accounts[i].getUuid();
+ }
+ } else {
+ mAccountUuids = accountUuids;
+ }
+ }
}
private void initializeMessageList() {
mAdapter = new MessageListAdapter();
if (mFolderName != null) {
- mCurrentFolder = mAdapter.getFolder(mFolderName, mAccount);
+ mCurrentFolder = getFolder(mFolderName, mAccount);
}
- // Hide "Load up to x more" footer for search views
- mFooterView.setVisibility((mQueryString != null) ? View.GONE : View.VISIBLE);
+ if (mSingleFolderMode) {
+ mListView.addFooterView(getFooterView(mListView));
+ updateFooterView();
+ }
mController = MessagingController.getInstance(getActivity().getApplication());
mListView.setAdapter(mAdapter);
}
+ private FolderInfoHolder getFolder(String folder, Account account) {
+ LocalFolder local_folder = null;
+ try {
+ LocalStore localStore = account.getLocalStore();
+ local_folder = localStore.getFolder(folder);
+ return new FolderInfoHolder(mContext, local_folder, account);
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e);
+ return null;
+ } finally {
+ if (local_folder != null) {
+ local_folder.close();
+ }
+ }
+ }
+
+ private String getFolderNameById(Account account, long folderId) {
+ try {
+ Folder folder = getFolderById(account, folderId);
+ if (folder != null) {
+ return folder.getName();
+ }
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e);
+ }
+
+ return null;
+ }
+
+ private Folder getFolderById(Account account, long folderId) {
+ try {
+ LocalStore localStore = account.getLocalStore();
+ LocalFolder localFolder = localStore.getFolderById(folderId);
+ localFolder.open(OpenMode.READ_ONLY);
+ return localFolder;
+ } catch (Exception e) {
+ Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e);
+ return null;
+ }
+ }
+
@Override
public void onPause() {
super.onPause();
- mController.removeListener(mAdapter.mListener);
+ mController.removeListener(mListener);
saveListState();
}
@@ -821,16 +883,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mSenderAboveSubject = K9.messageListSenderAboveSubject();
- final Preferences prefs = Preferences.getPreferences(appContext);
-
- boolean allowRemoteSearch = false;
- if (mSearchAccount != null) {
- final Account searchAccount = prefs.getAccount(mSearchAccount);
- if (searchAccount != null) {
- allowRemoteSearch = searchAccount.allowRemoteSearch();
- }
- }
-
// Check if we have connectivity. Cache the value.
if (mHasConnectivity == null) {
final ConnectivityManager connectivityManager =
@@ -844,100 +896,47 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
}
- if (mQueryString == null) {
- mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
- @Override
- public void onRefresh(PullToRefreshBase refreshView) {
- checkMail();
- }
- });
- } else if (allowRemoteSearch && !mRemoteSearch && !mIntegrate && mHasConnectivity) {
- // mQueryString != null is implied if we get this far.
- mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
- @Override
- public void onRefresh(PullToRefreshBase refreshView) {
- mPullToRefreshView.onRefreshComplete();
- onRemoteSearchRequested(true);
- }
- });
- mPullToRefreshView.setPullLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_pull));
- mPullToRefreshView.setReleaseLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_release));
+ if (mSingleFolderMode) {
+ if (mSearch.isManualSearch() && mAccount.allowRemoteSearch()) {
+ mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
+ @Override
+ public void onRefresh(PullToRefreshBase refreshView) {
+ mPullToRefreshView.onRefreshComplete();
+ onRemoteSearchRequested();
+ }
+ });
+ mPullToRefreshView.setPullLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_pull));
+ mPullToRefreshView.setReleaseLabel(getString(R.string.pull_to_refresh_remote_search_from_local_search_release));
+ } else {
+ mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
+ @Override
+ public void onRefresh(PullToRefreshBase refreshView) {
+ checkMail();
+ }
+ });
+ }
} else {
mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED);
}
- mController.addListener(mAdapter.mListener);
+ mController.addListener(mListener);
//Cancel pending new mail notifications when we open an account
Account[] accountsWithNotification;
- Account account = getCurrentAccount(prefs);
-
+ Account account = mAccount;
if (account != null) {
accountsWithNotification = new Account[] { account };
- mSortType = account.getSortType();
- mSortAscending = account.isSortAscending(mSortType);
- mSortDateAscending = account.isSortAscending(SortType.SORT_DATE);
} else {
- accountsWithNotification = prefs.getAccounts();
- mSortType = K9.getSortType();
- mSortAscending = K9.isSortAscending(mSortType);
- mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
+ accountsWithNotification = mPreferences.getAccounts();
}
for (Account accountWithNotification : accountsWithNotification) {
mController.notifyAccountCancel(appContext, accountWithNotification);
}
- if (mAdapter.isEmpty()) {
- if (mRemoteSearch) {
- //TODO: Support flag based search
- mRemoteSearchFuture = mController.searchRemoteMessages(mSearchAccount, mSearchFolder, mQueryString, null, null, mAdapter.mListener);
- } else if (mFolderName != null) {
- mController.listLocalMessages(mAccount, mFolderName, mAdapter.mListener);
- // Hide the archive button if we don't have an archive folder.
- if (!mAccount.hasArchiveFolder()) {
-// mBatchArchiveButton.setVisibility(View.GONE);
- }
- } else if (mQueryString != null) {
- mController.searchLocalMessages(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener);
- // Don't show the archive button if this is a search.
-// mBatchArchiveButton.setVisibility(View.GONE);
- }
-
- } else {
- // reread the selected date format preference in case it has changed
- mMessageHelper.refresh();
-
- mAdapter.markAllMessagesAsDirty();
-
- if (!mRemoteSearch) {
- new Thread() {
- @Override
- public void run() {
- if (mFolderName != null) {
- mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener);
- } else if (mQueryString != null) {
- mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener);
- }
-
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- mAdapter.pruneDirtyMessages();
- mAdapter.notifyDataSetChanged();
- restoreListState();
- }
- });
- }
-
- }
- .start();
- }
- }
-
- if (mAccount != null && mFolderName != null && !mRemoteSearch) {
- mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener);
+ if (mAccount != null && mFolderName != null && !mSearch.isManualSearch()) {
+ mController.getFolderUnreadMessageCount(mAccount, mFolderName, mListener);
}
refreshTitle();
@@ -950,17 +949,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mListView.setFastScrollEnabled(true);
mListView.setScrollingCacheEnabled(false);
mListView.setOnItemClickListener(this);
- mListView.addFooterView(getFooterView(mListView));
registerForContextMenu(mListView);
}
- private void onOpenMessage(MessageInfoHolder message) {
- mFragmentListener.openMessage(message.message.makeMessageReference());
+ private void onOpenMessage(MessageReference reference) {
+ mFragmentListener.openMessage(reference);
}
public void onCompose() {
- if (mQueryString != null) {
+ if (!mSingleAccountMode) {
/*
* If we have a query string, we don't have an account to let
* compose start the default action.
@@ -971,20 +969,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
}
- public void onReply(MessageInfoHolder holder) {
- mFragmentListener.onReply(holder.message);
+ public void onReply(Message message) {
+ mFragmentListener.onReply(message);
}
- public void onReplyAll(MessageInfoHolder holder) {
- mFragmentListener.onReplyAll(holder.message);
+ public void onReplyAll(Message message) {
+ mFragmentListener.onReplyAll(message);
}
- public void onForward(MessageInfoHolder holder) {
- mFragmentListener.onForward(holder.message);
+ public void onForward(Message message) {
+ mFragmentListener.onForward(message);
}
- public void onResendMessage(MessageInfoHolder holder) {
- mFragmentListener.onResendMessage(holder.message);
+ public void onResendMessage(Message message) {
+ mFragmentListener.onResendMessage(message);
}
public void changeSort(SortType sortType) {
@@ -994,22 +992,21 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
/**
* User has requested a remote search. Setup the bundle and start the intent.
- * @param fromLocalSearch true if this is being called from a local search result screen. This affects
- * where we pull the account and folder info used for the next search.
*/
- public void onRemoteSearchRequested(final boolean fromLocalSearch) {
+ public void onRemoteSearchRequested() {
String searchAccount;
String searchFolder;
- if (fromLocalSearch) {
- searchAccount = mSearchAccount;
- searchFolder = mSearchFolder;
- } else {
- searchAccount = mAccount.getUuid();
- searchFolder = mCurrentFolder.name;
- }
+ searchAccount = mAccount.getUuid();
+ searchFolder = mCurrentFolder.name;
- mFragmentListener.remoteSearch(searchAccount, searchFolder, mQueryString);
+ String queryString = mSearch.getRemoteSearchArguments();
+
+ mRemoteSearchPerformed = true;
+ mRemoteSearchFuture = mController.searchRemoteMessages(searchAccount, searchFolder,
+ queryString, null, null, mListener);
+
+ mFragmentListener.remoteSearchStarted();
}
/**
@@ -1025,8 +1022,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private void changeSort(SortType sortType, Boolean sortAscending) {
mSortType = sortType;
- Preferences prefs = Preferences.getPreferences(getActivity().getApplicationContext());
- Account account = getCurrentAccount(prefs);
+ Account account = mAccount;
if (account != null) {
account.setSortType(mSortType);
@@ -1039,7 +1035,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
account.setSortAscending(mSortType, mSortAscending);
mSortDateAscending = account.isSortAscending(SortType.SORT_DATE);
- account.save(prefs);
+ account.save(mPreferences);
} else {
K9.setSortType(mSortType);
@@ -1051,7 +1047,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
K9.setSortAscending(mSortType, mSortAscending);
mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
- Editor editor = prefs.getPreferences().edit();
+ Editor editor = mPreferences.getPreferences().edit();
K9.save(editor);
editor.commit();
}
@@ -1065,7 +1061,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
Toast toast = Toast.makeText(getActivity(), toastString, Toast.LENGTH_SHORT);
toast.show();
- mAdapter.sortMessages();
+ LoaderManager loaderManager = getLoaderManager();
+ for (int i = 0, len = mAccountUuids.length; i < len; i++) {
+ loaderManager.restartLoader(i, null, this);
+ }
}
public void onCycleSort() {
@@ -1088,17 +1087,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
changeSort(sorts[curIndex]);
}
- /**
- * @param holders
- * Never {@code null}.
- */
- private void onDelete(final List holders) {
- final List messagesToRemove = new ArrayList();
- for (MessageInfoHolder holder : holders) {
- messagesToRemove.add(holder.message);
+ private void onDelete(Message message) {
+ onDelete(Collections.singletonList(message));
+ }
+
+ private void onDelete(List messages) {
+ if (mThreadedList) {
+ mController.deleteThreads(messages);
+ } else {
+ mController.deleteMessages(messages, null);
}
- mAdapter.removeMessages(holders);
- mController.deleteMessages(messagesToRemove.toArray(EMPTY_MESSAGE_ARRAY), null);
}
@Override
@@ -1115,22 +1113,22 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER);
- final List holders = mActiveMessages;
+ final List messages = mActiveMessages;
if (destFolderName != null) {
mActiveMessages = null; // don't need it any more
- final Account account = holders.get(0).message.getFolder().getAccount();
+ final Account account = messages.get(0).getFolder().getAccount();
account.setLastSelectedFolderName(destFolderName);
switch (requestCode) {
case ACTIVITY_CHOOSE_FOLDER_MOVE:
- move(holders, destFolderName);
+ move(messages, destFolderName);
break;
case ACTIVITY_CHOOSE_FOLDER_COPY:
- copy(holders, destFolderName);
+ copy(messages, destFolderName);
break;
}
}
@@ -1196,10 +1194,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
changeSort(SortType.SORT_SUBJECT);
return true;
}
- case R.id.set_sort_sender: {
- changeSort(SortType.SORT_SENDER);
- return true;
- }
+// case R.id.set_sort_sender: {
+// changeSort(SortType.SORT_SENDER);
+// return true;
+// }
case R.id.set_sort_flag: {
changeSort(SortType.SORT_FLAGGED);
return true;
@@ -1213,12 +1211,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
return true;
}
case R.id.select_all: {
- setSelectionState(true);
+ selectAll();
return true;
}
}
- if (mQueryString != null) {
+ if (!mSingleAccountMode) {
// None of the options after this point are "safe" for search results
//TODO: This is not true for "unread" and "starred" searches in regular folders
return false;
@@ -1242,110 +1240,143 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
public void onSendPendingMessages() {
- mController.sendPendingMessages(mAccount, mAdapter.mListener);
+ mController.sendPendingMessages(mAccount, null);
}
@Override
public boolean onContextItemSelected(android.view.MenuItem item) {
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo();
- final MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(info.position);
+ int adapterPosition = listViewToAdapterPosition(info.position);
+ Message message = getMessageAtPosition(adapterPosition);
-
- final List selection = getSelectionFromMessage(message);
- switch (item.getItemId()) {
- case R.id.reply: {
- onReply(message);
- break;
- }
- case R.id.reply_all: {
- onReplyAll(message);
- break;
- }
- case R.id.forward: {
- onForward(message);
- break;
- }
- case R.id.send_again: {
- onResendMessage(message);
- mSelectedCount = 0;
- break;
- }
- case R.id.same_sender: {
- mFragmentListener.showMoreFromSameSender(message.senderAddress);
- break;
- }
- case R.id.delete: {
- onDelete(selection);
- break;
- }
- case R.id.mark_as_read: {
- setFlag(selection, Flag.SEEN, true);
- break;
- }
- case R.id.mark_as_unread: {
- setFlag(selection, Flag.SEEN, false);
- break;
- }
- case R.id.flag: {
- setFlag(selection, Flag.FLAGGED, true);
- break;
- }
- case R.id.unflag: {
- setFlag(selection, Flag.FLAGGED, false);
- break;
- }
-
- // only if the account supports this
- case R.id.archive: {
- onArchive(selection);
- break;
- }
- case R.id.spam: {
- onSpam(selection);
- break;
- }
- case R.id.move: {
- onMove(selection);
- break;
- }
- case R.id.copy: {
- onCopy(selection);
- break;
+ switch (item.getItemId()) {
+ case R.id.reply: {
+ onReply(message);
+ break;
+ }
+ case R.id.reply_all: {
+ onReplyAll(message);
+ break;
+ }
+ case R.id.forward: {
+ onForward(message);
+ break;
+ }
+ case R.id.send_again: {
+ onResendMessage(message);
+ mSelectedCount = 0;
+ break;
+ }
+ case R.id.same_sender: {
+ Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
+ String senderAddress = getSenderAddressFromCursor(cursor);
+ if (senderAddress != null) {
+ mFragmentListener.showMoreFromSameSender(senderAddress);
}
+ break;
+ }
+ case R.id.delete: {
+ onDelete(message);
+ break;
+ }
+ case R.id.mark_as_read: {
+ setFlag(message, Flag.SEEN, true);
+ break;
+ }
+ case R.id.mark_as_unread: {
+ setFlag(message, Flag.SEEN, false);
+ break;
+ }
+ case R.id.flag: {
+ setFlag(message, Flag.FLAGGED, true);
+ break;
+ }
+ case R.id.unflag: {
+ setFlag(message, Flag.FLAGGED, false);
+ break;
}
- return true;
+ // only if the account supports this
+ case R.id.archive: {
+ onArchive(message);
+ break;
+ }
+ case R.id.spam: {
+ onSpam(message);
+ break;
+ }
+ case R.id.move: {
+ onMove(message);
+ break;
+ }
+ case R.id.copy: {
+ onCopy(message);
+ break;
+ }
}
+ return true;
+ }
+
+
+ private String getSenderAddressFromCursor(Cursor cursor) {
+ String fromList = cursor.getString(SENDER_LIST_COLUMN);
+ Address[] fromAddrs = Address.unpack(fromList);
+ return (fromAddrs.length > 0) ? fromAddrs[0].getAddress() : null;
+ }
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo;
- MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(info.position);
+ Cursor cursor = (Cursor) mListView.getItemAtPosition(info.position);
- if (message == null) {
+ if (cursor == null) {
return;
}
getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu);
- menu.setHeaderTitle(message.message.getSubject());
+ Account account = getAccountFromCursor(cursor);
- if (message.read) {
+ String subject = cursor.getString(SUBJECT_COLUMN);
+ String flagList = cursor.getString(FLAGS_COLUMN);
+ String[] flags = flagList.split(",");
+ boolean read = false;
+ boolean flagged = false;
+ for (int i = 0, len = flags.length; i < len; i++) {
+ try {
+ switch (Flag.valueOf(flags[i])) {
+ case SEEN: {
+ read = true;
+ break;
+ }
+ case FLAGGED: {
+ flagged = true;
+ break;
+ }
+ default: {
+ // We don't care about the other flags
+ }
+ }
+ } catch (Exception e) { /* ignore */ }
+ }
+
+ menu.setHeaderTitle(subject);
+
+ if (read) {
menu.findItem(R.id.mark_as_read).setVisible(false);
} else {
menu.findItem(R.id.mark_as_unread).setVisible(false);
}
- if (message.flagged) {
+ if (flagged) {
menu.findItem(R.id.flag).setVisible(false);
} else {
menu.findItem(R.id.unflag).setVisible(false);
}
- Account account = message.message.getFolder().getAccount();
if (!mController.isCopyCapable(account)) {
menu.findItem(R.id.copy).setVisible(false);
}
@@ -1377,216 +1408,137 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
/**
- * Handle a select or unselect swipe event
- * @param downMotion Event that started the swipe
- * @param selected true if this was an attempt to select (i.e. left to right).
+ * Handle a select or unselect swipe event.
+ *
+ * @param downMotion
+ * Event that started the swipe
+ * @param selected
+ * {@code true} if this was an attempt to select (i.e. left to right).
*/
private void handleSwipe(final MotionEvent downMotion, final boolean selected) {
int[] listPosition = new int[2];
mListView.getLocationOnScreen(listPosition);
- int position = mListView.pointToPosition((int) downMotion.getRawX() - listPosition[0], (int) downMotion.getRawY() - listPosition[1]);
- if (position != AdapterView.INVALID_POSITION) {
- final MessageInfoHolder message = (MessageInfoHolder) mListView.getItemAtPosition(position);
- toggleMessageSelect(message);
+
+ int listX = (int) downMotion.getRawX() - listPosition[0];
+ int listY = (int) downMotion.getRawY() - listPosition[1];
+
+ int listViewPosition = mListView.pointToPosition(listX, listY);
+
+ toggleMessageSelect(listViewPosition);
+ }
+
+ private int listViewToAdapterPosition(int position) {
+ if (position > 0 && position <= mAdapter.getCount()) {
+ return position - 1;
+ }
+
+ return AdapterView.INVALID_POSITION;
+ }
+
+ class MessageListActivityListener extends ActivityListener {
+ @Override
+ public void remoteSearchFailed(Account acct, String folder, final String err) {
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ Activity activity = getActivity();
+ if (activity != null) {
+ Toast.makeText(activity, R.string.remote_search_error,
+ Toast.LENGTH_LONG).show();
+ }
+ }
+ });
+ }
+
+ @Override
+ public void remoteSearchStarted(Account acct, String folder) {
+ mHandler.progress(true);
+ mHandler.updateFooter(mContext.getString(R.string.remote_search_sending_query), true);
+ }
+
+
+ @Override
+ public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) {
+ mHandler.progress(false);
+ mHandler.remoteSearchFinished();
+ mExtraSearchResults = extraResults;
+ if (extraResults != null && extraResults.size() > 0) {
+ mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false);
+ } else {
+ mHandler.updateFooter("", false);
+ }
+ mFragmentListener.setMessageListProgress(Window.PROGRESS_END);
+
+ }
+
+ @Override
+ public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) {
+ mHandler.progress(true);
+ if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) {
+ mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true);
+ } else {
+ mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading, numResults), true);
+ }
+ mFragmentListener.setMessageListProgress(Window.PROGRESS_START);
+ }
+
+ @Override
+ public void informUserOfStatus() {
+ mHandler.refreshTitle();
+ }
+
+ @Override
+ public void synchronizeMailboxStarted(Account account, String folder) {
+ if (updateForMe(account, folder)) {
+ mHandler.progress(true);
+ mHandler.folderLoading(folder, true);
+ }
+ super.synchronizeMailboxStarted(account, folder);
+ }
+
+ @Override
+ public void synchronizeMailboxFinished(Account account, String folder,
+ int totalMessagesInMailbox, int numNewMessages) {
+
+ if (updateForMe(account, folder)) {
+ mHandler.progress(false);
+ mHandler.folderLoading(folder, false);
+ }
+ super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages);
+ }
+
+ @Override
+ public void synchronizeMailboxFailed(Account account, String folder, String message) {
+
+ if (updateForMe(account, folder)) {
+ mHandler.progress(false);
+ mHandler.folderLoading(folder, false);
+ }
+ super.synchronizeMailboxFailed(account, folder, message);
+ }
+
+ @Override
+ public void searchStats(AccountStats stats) {
+ mUnreadMessageCount = stats.unreadMessageCount;
+ super.searchStats(stats);
+ }
+
+ @Override
+ public void folderStatusChanged(Account account, String folder, int unreadMessageCount) {
+ if (updateForMe(account, folder)) {
+ mUnreadMessageCount = unreadMessageCount;
+ }
+ super.folderStatusChanged(account, folder, unreadMessageCount);
+ }
+
+ private boolean updateForMe(Account account, String folder) {
+ //FIXME
+ return ((account.equals(mAccount) && folder.equals(mFolderName)));
}
}
- class MessageListAdapter extends BaseAdapter {
- private final List mMessages =
- Collections.synchronizedList(new ArrayList());
- public List mExtraSearchResults;
-
- private final ActivityListener mListener = new ActivityListener() {
-
- @Override
- public void remoteSearchAddMessage(Account account, String folderName, Message message, final int numDone, final int numTotal) {
-
- if (numTotal > 0 && numDone < numTotal) {
- mFragmentListener.setMessageListProgress(Window.PROGRESS_END / numTotal * numDone);
- } else {
- mFragmentListener.setMessageListProgress(Window.PROGRESS_END);
- }
-
- mHandler.addOrUpdateMessages(account, folderName, Collections.singletonList(message), false);
- }
-
- @Override
- public void remoteSearchFailed(Account acct, String folder, final String err) {
- //TODO: Better error handling
- mHandler.post(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(getActivity(), err, Toast.LENGTH_LONG).show();
- }
- });
- }
-
- @Override
- public void remoteSearchStarted(Account acct, String folder) {
- mHandler.progress(true);
- mHandler.updateFooter(getString(R.string.remote_search_sending_query), true);
- }
-
-
- @Override
- public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) {
- mHandler.progress(false);
- if (extraResults != null && extraResults.size() > 0) {
- mExtraSearchResults = extraResults;
- mHandler.updateFooter(String.format(getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false);
- } else {
- mHandler.updateFooter("", false);
- }
- mFragmentListener.setMessageListProgress(Window.PROGRESS_END);
-
- }
-
- @Override
- public void remoteSearchServerQueryComplete(Account account, String folderName, int numResults) {
- mHandler.progress(true);
- if (account != null && account.getRemoteSearchNumResults() != 0 && numResults > account.getRemoteSearchNumResults()) {
- mHandler.updateFooter(getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true);
- } else {
- mHandler.updateFooter(getString(R.string.remote_search_downloading, numResults), true);
- }
- mFragmentListener.setMessageListProgress(Window.PROGRESS_START);
- }
-
- @Override
- public void informUserOfStatus() {
- mHandler.refreshTitle();
- }
-
- @Override
- public void synchronizeMailboxStarted(Account account, String folder) {
- if (updateForMe(account, folder)) {
- mHandler.progress(true);
- mHandler.folderLoading(folder, true);
- }
- super.synchronizeMailboxStarted(account, folder);
- }
-
- @Override
- public void synchronizeMailboxFinished(Account account, String folder,
- int totalMessagesInMailbox, int numNewMessages) {
-
- if (updateForMe(account, folder)) {
- mHandler.progress(false);
- mHandler.folderLoading(folder, false);
- mHandler.sortMessages();
- }
- super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages);
- }
-
- @Override
- public void synchronizeMailboxFailed(Account account, String folder, String message) {
-
- if (updateForMe(account, folder)) {
- mHandler.progress(false);
- mHandler.folderLoading(folder, false);
- mHandler.sortMessages();
- }
- super.synchronizeMailboxFailed(account, folder, message);
- }
-
- @Override
- public void synchronizeMailboxAddOrUpdateMessage(Account account, String folder, Message message) {
- mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), true);
- }
-
- @Override
- public void synchronizeMailboxRemovedMessage(Account account, String folder, Message message) {
- mHandler.removeMessage(message.makeMessageReference());
- }
-
- @Override
- public void listLocalMessagesStarted(Account account, String folder) {
- if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
- mHandler.progress(true);
- if (folder != null) {
- mHandler.folderLoading(folder, true);
- }
- }
- }
-
- @Override
- public void listLocalMessagesFailed(Account account, String folder, String message) {
- if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
- mHandler.sortMessages();
- mHandler.progress(false);
- if (folder != null) {
- mHandler.folderLoading(folder, false);
- }
- }
- }
-
- @Override
- public void listLocalMessagesFinished(Account account, String folder) {
- if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
- mHandler.sortMessages();
- mHandler.progress(false);
- if (folder != null) {
- mHandler.folderLoading(folder, false);
- }
- }
- }
-
- @Override
- public void listLocalMessagesRemoveMessage(Account account, String folder, Message message) {
- mHandler.removeMessage(message.makeMessageReference());
- }
-
- @Override
- public void listLocalMessagesAddMessages(Account account, String folder, List messages) {
- mHandler.addOrUpdateMessages(account, folder, messages, false);
- }
-
- @Override
- public void listLocalMessagesUpdateMessage(Account account, String folder, Message message) {
- mHandler.addOrUpdateMessages(account, folder, Collections.singletonList(message), false);
- }
-
- @Override
- public void searchStats(AccountStats stats) {
- mUnreadMessageCount = stats.unreadMessageCount;
- super.searchStats(stats);
- }
-
- @Override
- public void folderStatusChanged(Account account, String folder, int unreadMessageCount) {
- if (updateForMe(account, folder)) {
- mUnreadMessageCount = unreadMessageCount;
- }
- super.folderStatusChanged(account, folder, unreadMessageCount);
- }
-
- @Override
- public void messageUidChanged(Account account, String folder, String oldUid, String newUid) {
- MessageReference ref = new MessageReference();
- ref.accountUuid = account.getUuid();
- ref.folderName = folder;
- ref.uid = oldUid;
-
- mHandler.changeMessageUid(ref, newUid);
- }
- };
-
- private boolean updateForMe(Account account, String folder) {
- if ((account.equals(mAccount) && mFolderName != null && folder.equals(mFolderName))) {
- return true;
- } else {
- return false;
- }
- }
-
- public List getMessages() {
- return mMessages;
- }
-
- public void restoreMessages(List messages) {
- mMessages.addAll(messages);
- }
+ class MessageListAdapter extends CursorAdapter {
private Drawable mAttachmentIcon;
private Drawable mForwardedIcon;
@@ -1594,542 +1546,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
private Drawable mForwardedAnsweredIcon;
MessageListAdapter() {
+ super(getActivity(), null, 0);
mAttachmentIcon = getResources().getDrawable(R.drawable.ic_email_attachment_small);
mAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_answered_small);
mForwardedIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_small);
mForwardedAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_answered_small);
}
- public void markAllMessagesAsDirty() {
- for (MessageInfoHolder holder : mMessages) {
- holder.dirty = true;
- }
- }
-
- public void pruneDirtyMessages() {
- List messagesToRemove = new ArrayList();
-
- for (MessageInfoHolder holder : mMessages) {
- if (holder.dirty) {
- messagesToRemove.add(holder);
- }
- }
- removeMessages(messagesToRemove);
- }
-
- public void removeMessage(MessageReference messageReference) {
- MessageInfoHolder holder = getMessage(messageReference);
- if (holder == null) {
- Log.w(K9.LOG_TAG, "Got callback to remove non-existent message with UID " +
- messageReference.uid);
- } else {
- removeMessages(Collections.singletonList(holder));
- }
- }
-
- public void removeMessages(final List messages) {
- if (messages.isEmpty()) {
- return;
- }
-
- for (MessageInfoHolder message : messages) {
- if (message != null && (mFolderName == null || (
- message.folder != null &&
- message.folder.name.equals(mFolderName)))) {
- if (message.selected && mSelectedCount > 0) {
- mSelectedCount--;
- }
- mMessages.remove(message);
- }
- }
- resetUnreadCount();
-
- notifyDataSetChanged();
- computeSelectAllVisibility();
- }
-
- /**
- * Set the selection state for all messages at once.
- * @param selected Selection state to set.
- */
- public void setSelectionForAllMesages(final boolean selected) {
- for (MessageInfoHolder message : mMessages) {
- message.selected = selected;
- }
-
- notifyDataSetChanged();
- }
-
- public void addMessages(final List messages) {
- if (messages.isEmpty()) {
- return;
- }
-
- final boolean wasEmpty = mMessages.isEmpty();
-
- for (final MessageInfoHolder message : messages) {
- if (mFolderName == null || (message.folder != null && message.folder.name.equals(mFolderName))) {
- int index = Collections.binarySearch(mMessages, message, getComparator());
-
- if (index < 0) {
- index = (index * -1) - 1;
- }
-
- mMessages.add(index, message);
- }
- }
-
- if (wasEmpty) {
- mListView.setSelection(0);
- }
- resetUnreadCount();
-
- notifyDataSetChanged();
- computeSelectAllVisibility();
- }
-
- public void changeMessageUid(MessageReference ref, String newUid) {
- MessageInfoHolder holder = getMessage(ref);
- if (holder != null) {
- holder.uid = newUid;
- holder.message.setUid(newUid);
- }
- }
-
- public void resetUnreadCount() {
- if (mQueryString != null) {
- int unreadCount = 0;
-
- for (MessageInfoHolder holder : mMessages) {
- unreadCount += holder.read ? 0 : 1;
- }
-
- mUnreadMessageCount = unreadCount;
- refreshTitle();
- }
- }
-
- public void sortMessages() {
- final Comparator chainComparator = getComparator();
-
- Collections.sort(mMessages, chainComparator);
-
- notifyDataSetChanged();
- }
-
- public void addOrUpdateMessages(final Account account, final String folderName,
- final List messages, final boolean verifyAgainstSearch) {
-
- boolean needsSort = false;
- final List messagesToAdd = new ArrayList();
- List messagesToRemove = new ArrayList();
- List messagesToSearch = new ArrayList();
-
- // cache field into local variable for faster access for JVM without JIT
- final MessageHelper messageHelper = mMessageHelper;
-
- for (Message message : messages) {
- MessageInfoHolder m = getMessage(message);
- if (message.isSet(Flag.DELETED)) {
- if (m != null) {
- messagesToRemove.add(m);
- }
- } else {
- final Folder messageFolder = message.getFolder();
- final Account messageAccount = messageFolder.getAccount();
- if (m == null) {
- if (updateForMe(account, folderName)) {
- m = new MessageInfoHolder();
- FolderInfoHolder folderInfoHolder = new FolderInfoHolder(
- getActivity(), messageFolder, messageAccount);
- messageHelper.populate(m, message, folderInfoHolder, messageAccount);
- messagesToAdd.add(m);
- } else {
- if (mQueryString != null) {
- if (verifyAgainstSearch) {
- messagesToSearch.add(message);
- } else {
- m = new MessageInfoHolder();
- FolderInfoHolder folderInfoHolder = new FolderInfoHolder(
- getActivity(), messageFolder, messageAccount);
- messageHelper.populate(m, message, folderInfoHolder,
- messageAccount);
- messagesToAdd.add(m);
- }
- }
- }
- } else {
- m.dirty = false; // as we reload the message, unset its dirty flag
- FolderInfoHolder folderInfoHolder = new FolderInfoHolder(getActivity(),
- messageFolder, account);
- messageHelper.populate(m, message, folderInfoHolder, account);
- needsSort = true;
- }
- }
- }
-
- if (!messagesToSearch.isEmpty()) {
- mController.searchLocalMessages(mAccountUuids, mFolderNames,
- messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY), mQueryString, mIntegrate,
- mQueryFlags, mForbiddenFlags,
- new MessagingListener() {
- @Override
- public void listLocalMessagesAddMessages(Account account, String folder,
- List messages) {
- mHandler.addOrUpdateMessages(account, folder, messages, false);
- }
- });
- }
-
- if (!messagesToRemove.isEmpty()) {
- removeMessages(messagesToRemove);
- }
-
- if (!messagesToAdd.isEmpty()) {
- addMessages(messagesToAdd);
- }
-
- if (needsSort) {
- sortMessages();
- resetUnreadCount();
- }
- }
-
- /**
- * Find a specific message in the message list.
- *
- * Note:
- * This method was optimized because it is called a lot. Don't change it unless you know
- * what you are doing.
- *
- * @param message
- * A {@link Message} instance describing the message to look for.
- *
- * @return The corresponding {@link MessageInfoHolder} instance if the message was found in
- * the message list. {@code null} otherwise.
- */
- private MessageInfoHolder getMessage(Message message) {
- String uid;
- Folder folder;
- for (MessageInfoHolder holder : mMessages) {
- uid = message.getUid();
- if (uid != null && (holder.uid == uid || uid.equals(holder.uid))) {
- folder = message.getFolder();
- if (holder.folder.name.equals(folder.getName()) &&
- holder.account.equals(folder.getAccount().getUuid())) {
- return holder;
- }
- }
- }
-
- return null;
- }
-
- /**
- * Find a specific message in the message list.
- *
- * Note:
- * This method was optimized because it is called a lot. Don't change it unless you know
- * what you are doing.
- *
- * @param messageReference
- * A {@link MessageReference} instance describing the message to look for.
- *
- * @return The corresponding {@link MessageInfoHolder} instance if the message was found in
- * the message list. {@code null} otherwise.
- */
- private MessageInfoHolder getMessage(MessageReference messageReference) {
- String uid;
- for (MessageInfoHolder holder : mMessages) {
- uid = messageReference.uid;
- if ((holder.uid == uid || uid.equals(holder.uid)) &&
- holder.folder.name.equals(messageReference.folderName) &&
- holder.account.equals(messageReference.accountUuid)) {
- return holder;
- }
- }
-
- return null;
- }
-
- public FolderInfoHolder getFolder(String folder, Account account) {
- LocalFolder local_folder = null;
- try {
- LocalStore localStore = account.getLocalStore();
- local_folder = localStore.getFolder(folder);
- return new FolderInfoHolder(getActivity(), local_folder, account);
- } catch (Exception e) {
- Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e);
- return null;
- } finally {
- if (local_folder != null) {
- local_folder.close();
- }
- }
- }
-
-
- @Override
- public int getCount() {
- return mMessages.size();
- }
-
- @Override
- public long getItemId(int position) {
- try {
- MessageInfoHolder messageHolder = (MessageInfoHolder) getItem(position);
- if (messageHolder != null) {
- return messageHolder.message.getId();
- }
- } catch (Exception e) {
- Log.i(K9.LOG_TAG, "getItemId(" + position + ") ", e);
- }
- return -1;
- }
-
- @Override
- public Object getItem(int position) {
- try {
- if (position < mMessages.size()) {
- return mMessages.get(position);
- }
- } catch (Exception e) {
- Log.e(K9.LOG_TAG, "getItem(" + position + "), but folder.messages.size() = " + mMessages.size(), e);
- }
- return null;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
- MessageInfoHolder message = (MessageInfoHolder) getItem(position);
- View view;
-
- if ((convertView != null) && (convertView.getId() == R.layout.message_list_item)) {
- view = convertView;
- } else {
- view = mInflater.inflate(R.layout.message_list_item, parent, false);
- view.setId(R.layout.message_list_item);
- }
-
- MessageViewHolder holder = (MessageViewHolder) view.getTag();
-
- if (holder == null) {
- holder = new MessageViewHolder();
- holder.date = (TextView) view.findViewById(R.id.date);
- holder.chip = view.findViewById(R.id.chip);
- holder.selected = (CheckBox) view.findViewById(R.id.selected_checkbox);
- holder.preview = (TextView) view.findViewById(R.id.preview);
- if (mCheckboxes) {
- holder.selected.setVisibility(View.VISIBLE);
- }
-
- if (holder.selected != null) {
- holder.selected.setOnCheckedChangeListener(holder);
- }
-
-
- if (mSenderAboveSubject) {
- holder.from = (TextView) view.findViewById(R.id.subject);
- holder.from.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSender());
- } else {
- holder.subject = (TextView) view.findViewById(R.id.subject);
- holder.subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSubject());
- }
-
- holder.date.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListDate());
-
- holder.preview.setLines(mPreviewLines);
- holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview());
-
- view.setTag(holder);
- }
-
- if (message != null) {
- bindView(position, view, holder, message);
- } else {
- // This branch code is triggered when the local store
- // hands us an invalid message
-
- holder.chip.getBackground().setAlpha(0);
- if (holder.subject != null) {
- holder.subject.setText(getString(R.string.general_no_subject));
- holder.subject.setTypeface(null, Typeface.NORMAL);
- }
-
- String noSender = getString(R.string.general_no_sender);
-
- if (holder.preview != null) {
- holder.preview.setText(noSender, TextView.BufferType.SPANNABLE);
- Spannable str = (Spannable) holder.preview.getText();
-
- str.setSpan(new StyleSpan(Typeface.NORMAL),
- 0,
- noSender.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- str.setSpan(new AbsoluteSizeSpan(mFontSizes.getMessageListSender(), true),
- 0,
- noSender.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- } else {
- holder.from.setText(noSender);
- holder.from.setTypeface(null, Typeface.NORMAL);
- holder.from.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
- }
-
- holder.date.setText(getString(R.string.general_no_date));
-
- //WARNING: Order of the next 2 lines matter
- holder.position = -1;
- holder.selected.setChecked(false);
-
- if (!mCheckboxes) {
- holder.selected.setVisibility(View.GONE);
- }
- }
-
-
- return view;
- }
-
- /**
- * Associate model data to view object.
- *
- * @param position
- * The position of the item within the adapter's data set of
- * the item whose view we want.
- * @param view
- * Main view component to alter. Never null
.
- * @param holder
- * Convenience view holder - eases access to view
- * child views. Never null
.
- * @param message
- * Never null
.
- */
-
-
-
- private void bindView(final int position, final View view, final MessageViewHolder holder,
- final MessageInfoHolder message) {
-
- int maybeBoldTypeface = message.read ? Typeface.NORMAL : Typeface.BOLD;
-
- // So that the mSelectedCount is only incremented/decremented
- // when a user checks the checkbox (vs code)
- holder.position = -1;
-
- holder.selected.setChecked(message.selected);
-
- if (!mCheckboxes && message.selected) {
-
- holder.chip.setBackgroundDrawable(message.message.getFolder().getAccount().getCheckmarkChip().drawable());
- }
-
- else {
- holder.chip.setBackgroundDrawable(message.message.getFolder().getAccount().generateColorChip(message.read,message.message.toMe(), message.message.ccMe(), message.message.fromMe(), message.flagged).drawable());
-
- }
-
- if (K9.useBackgroundAsUnreadIndicator()) {
- int res = (message.read) ? R.attr.messageListReadItemBackgroundColor :
- R.attr.messageListUnreadItemBackgroundColor;
-
- TypedValue outValue = new TypedValue();
- getActivity().getTheme().resolveAttribute(res, outValue, true);
- view.setBackgroundColor(outValue.data);
- }
-
- String subject = null;
-
- if ((message.message.getSubject() == null) || message.message.getSubject().equals("")) {
- subject = (String) getText(R.string.general_no_subject);
-
- } else {
- subject = message.message.getSubject();
- }
-
- // We'll get badge support soon --jrv
-// if (holder.badge != null) {
-// String email = message.counterpartyAddress;
-// holder.badge.assignContactFromEmail(email, true);
-// if (email != null) {
-// mContactsPictureLoader.loadContactPicture(email, holder.badge);
-// }
-// }
-
- if (holder.preview != null) {
- /*
- * In the touchable UI, we have previews. Otherwise, we
- * have just a "from" line.
- * Because text views can't wrap around each other(?) we
- * compose a custom view containing the preview and the
- * from.
- */
-
- CharSequence beforePreviewText = null;
- if (mSenderAboveSubject) {
- beforePreviewText = subject;
- } else {
- beforePreviewText = message.sender;
- }
-
- holder.preview.setText(new SpannableStringBuilder(recipientSigil(message))
- .append(beforePreviewText).append(" ").append(message.message.getPreview()),
- TextView.BufferType.SPANNABLE);
- Spannable str = (Spannable)holder.preview.getText();
-
- // Create a span section for the sender, and assign the correct font size and weight.
- str.setSpan(new AbsoluteSizeSpan((mSenderAboveSubject ? mFontSizes.getMessageListSubject(): mFontSizes.getMessageListSender()), true),
- 0, beforePreviewText.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
-
- int color = (K9.getK9Theme() == K9.THEME_LIGHT) ?
- Color.rgb(105, 105, 105) :
- Color.rgb(160, 160, 160);
-
- // set span for preview message.
- str.setSpan(new ForegroundColorSpan(color), // How do I can specify the android.R.attr.textColorTertiary
- beforePreviewText.length() + 1,
- str.length(),
- Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
- }
-
-
- if (holder.from != null ) {
- holder.from.setTypeface(null, maybeBoldTypeface);
- if (mSenderAboveSubject) {
- holder.from.setCompoundDrawablesWithIntrinsicBounds(
- message.answered ? mAnsweredIcon : null, // left
- null, // top
- message.message.hasAttachments() ? mAttachmentIcon : null, // right
- null); // bottom
-
- holder.from.setText(message.sender);
- } else {
- holder.from.setText(new SpannableStringBuilder(recipientSigil(message)).append(message.sender));
- }
- }
-
- if (holder.subject != null ) {
- if (!mSenderAboveSubject) {
- holder.subject.setCompoundDrawablesWithIntrinsicBounds(
- message.answered ? mAnsweredIcon : null, // left
- null, // top
- message.message.hasAttachments() ? mAttachmentIcon : null, // right
- null); // bottom
- }
-
- holder.subject.setTypeface(null, maybeBoldTypeface);
- holder.subject.setText(subject);
- }
-
- holder.date.setText(message.getDate(mMessageHelper));
- holder.position = position;
- }
-
-
- private String recipientSigil(MessageInfoHolder message) {
- if (message.message.toMe()) {
+ private String recipientSigil(boolean toMe, boolean ccMe) {
+ if (toMe) {
return getString(R.string.messagelist_sent_to_me_sigil);
- } else if (message.message.ccMe()) {
+ } else if (ccMe) {
return getString(R.string.messagelist_sent_cc_me_sigil);
} else {
return "";
@@ -2137,29 +1564,239 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
@Override
- public boolean hasStableIds() {
- return true;
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ View view = mInflater.inflate(R.layout.message_list_item, parent, false);
+ view.setId(R.layout.message_list_item);
+
+ MessageViewHolder holder = new MessageViewHolder();
+ holder.date = (TextView) view.findViewById(R.id.date);
+ holder.chip = view.findViewById(R.id.chip);
+ holder.preview = (TextView) view.findViewById(R.id.preview);
+
+ if (mSenderAboveSubject) {
+ holder.from = (TextView) view.findViewById(R.id.subject);
+ holder.from.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSender());
+ } else {
+ holder.subject = (TextView) view.findViewById(R.id.subject);
+ holder.subject.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListSubject());
+ }
+
+ holder.date.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListDate());
+
+ holder.preview.setLines(mPreviewLines);
+ holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview());
+ holder.threadCount = (TextView) view.findViewById(R.id.thread_count);
+
+ holder.selected = (CheckBox) view.findViewById(R.id.selected_checkbox);
+ if (mCheckboxes) {
+ holder.selected.setOnCheckedChangeListener(holder);
+ holder.selected.setVisibility(View.VISIBLE);
+ } else {
+ holder.selected.setVisibility(View.GONE);
+ }
+
+ view.setTag(holder);
+
+ return view;
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ Account account = getAccountFromCursor(cursor);
+
+ String fromList = cursor.getString(SENDER_LIST_COLUMN);
+ String toList = cursor.getString(TO_LIST_COLUMN);
+ String ccList = cursor.getString(CC_LIST_COLUMN);
+ Address[] fromAddrs = Address.unpack(fromList);
+ Address[] toAddrs = Address.unpack(toList);
+ Address[] ccAddrs = Address.unpack(ccList);
+
+ boolean fromMe = mMessageHelper.toMe(account, fromAddrs);
+ boolean toMe = mMessageHelper.toMe(account, toAddrs);
+ boolean ccMe = mMessageHelper.toMe(account, ccAddrs);
+
+ CharSequence displayName = mMessageHelper.getDisplayName(account, fromAddrs, toAddrs);
+
+ Date sentDate = new Date(cursor.getLong(DATE_COLUMN));
+ String displayDate = mMessageHelper.formatDate(sentDate);
+
+ String preview = cursor.getString(PREVIEW_COLUMN);
+ if (preview == null) {
+ preview = "";
+ }
+
+ String subject = cursor.getString(SUBJECT_COLUMN);
+ if (StringUtils.isNullOrEmpty(subject)) {
+ subject = getString(R.string.general_no_subject);
+ }
+
+ int threadCount = (mThreadedList) ? cursor.getInt(THREAD_COUNT_COLUMN) : 0;
+
+ String flagList = cursor.getString(FLAGS_COLUMN);
+ String[] flags = flagList.split(",");
+ boolean read = false;
+ boolean flagged = false;
+ boolean answered = false;
+ boolean forwarded = false;
+ for (int i = 0, len = flags.length; i < len; i++) {
+ try {
+ switch (Flag.valueOf(flags[i])) {
+ case SEEN: {
+ read = true;
+ break;
+ }
+ case FLAGGED: {
+ flagged = true;
+ break;
+ }
+ case ANSWERED: {
+ answered = true;
+ break;
+ }
+ case FORWARDED: {
+ forwarded = true;
+ break;
+ }
+ default: {
+ // We don't care about the other flags
+ }
+ }
+ } catch (Exception e) { /* ignore */ }
+ }
+
+ boolean hasAttachments = (cursor.getInt(ATTACHMENT_COUNT_COLUMN) > 0);
+
+ MessageViewHolder holder = (MessageViewHolder) view.getTag();
+
+ int maybeBoldTypeface = (read) ? Typeface.NORMAL : Typeface.BOLD;
+
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+ boolean selected = mSelected.contains(uniqueId);
+
+ if (!mCheckboxes && selected) {
+ holder.chip.setBackgroundDrawable(account.getCheckmarkChip().drawable());
+ } else {
+ holder.chip.setBackgroundDrawable(account.generateColorChip(read, toMe, ccMe,
+ fromMe, flagged).drawable());
+ }
+
+ if (mCheckboxes) {
+ // Set holder.position to -1 to avoid MessageViewHolder.onCheckedChanged() toggling
+ // the selection state when setChecked() is called below.
+ holder.position = -1;
+
+ // Only set the UI state, don't actually toggle the message selection.
+ holder.selected.setChecked(selected);
+
+ // Now save the position so MessageViewHolder.onCheckedChanged() will know what
+ // message to (de)select.
+ holder.position = cursor.getPosition();
+ }
+
+ // Background indicator
+ if (K9.useBackgroundAsUnreadIndicator()) {
+ int res = (read) ? R.attr.messageListReadItemBackgroundColor :
+ R.attr.messageListUnreadItemBackgroundColor;
+
+ TypedValue outValue = new TypedValue();
+ getActivity().getTheme().resolveAttribute(res, outValue, true);
+ view.setBackgroundColor(outValue.data);
+ }
+
+ // Thread count
+ if (threadCount > 1) {
+ holder.threadCount.setText(Integer.toString(threadCount));
+ holder.threadCount.setVisibility(View.VISIBLE);
+ } else {
+ holder.threadCount.setVisibility(View.GONE);
+ }
+
+ CharSequence beforePreviewText = (mSenderAboveSubject) ? subject : displayName;
+
+ String sigil = recipientSigil(toMe, ccMe);
+
+ holder.preview.setText(
+ new SpannableStringBuilder(sigil)
+ .append(beforePreviewText)
+ .append(" ")
+ .append(preview), TextView.BufferType.SPANNABLE);
+
+ Spannable str = (Spannable)holder.preview.getText();
+
+ // Create a span section for the sender, and assign the correct font size and weight
+ int fontSize = (mSenderAboveSubject) ?
+ mFontSizes.getMessageListSubject():
+ mFontSizes.getMessageListSender();
+
+ AbsoluteSizeSpan span = new AbsoluteSizeSpan(fontSize, true);
+ str.setSpan(span, 0, beforePreviewText.length() + 1,
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ //TODO: make this part of the theme
+ int color = (K9.getK9Theme() == K9.THEME_LIGHT) ?
+ Color.rgb(105, 105, 105) :
+ Color.rgb(160, 160, 160);
+
+ // Set span (color) for preview message
+ str.setSpan(new ForegroundColorSpan(color), beforePreviewText.length() + 1,
+ str.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+ Drawable statusHolder = null;
+ if (forwarded && answered) {
+ statusHolder = mForwardedAnsweredIcon;
+ } else if (answered) {
+ statusHolder = mAnsweredIcon;
+ } else if (forwarded) {
+ statusHolder = mForwardedIcon;
+ }
+
+ if (holder.from != null ) {
+ holder.from.setTypeface(null, maybeBoldTypeface);
+ if (mSenderAboveSubject) {
+ holder.from.setCompoundDrawablesWithIntrinsicBounds(
+ statusHolder, // left
+ null, // top
+ hasAttachments ? mAttachmentIcon : null, // right
+ null); // bottom
+
+ holder.from.setText(displayName);
+ } else {
+ holder.from.setText(new SpannableStringBuilder(sigil).append(displayName));
+ }
+ }
+
+ if (holder.subject != null ) {
+ if (!mSenderAboveSubject) {
+ holder.subject.setCompoundDrawablesWithIntrinsicBounds(
+ statusHolder, // left
+ null, // top
+ hasAttachments ? mAttachmentIcon : null, // right
+ null); // bottom
+ }
+
+ holder.subject.setTypeface(null, maybeBoldTypeface);
+ holder.subject.setText(subject);
+ }
+
+ holder.date.setText(displayDate);
}
}
- class MessageViewHolder
- implements OnCheckedChangeListener {
+ class MessageViewHolder implements OnCheckedChangeListener {
public TextView subject;
public TextView preview;
public TextView from;
public TextView time;
public TextView date;
public View chip;
+ public TextView threadCount;
public CheckBox selected;
public int position = -1;
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
if (position != -1) {
- MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem(position);
- toggleMessageSelect(message);
-
-
+ toggleMessageSelectWithAdapterPosition(position);
}
}
}
@@ -2180,20 +1817,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
private void updateFooterView() {
- if (mCurrentFolder != null && mAccount != null) {
+ if (!mSearch.isManualSearch() && mCurrentFolder != null && mAccount != null) {
if (mCurrentFolder.loading) {
final boolean showProgress = true;
- updateFooter(getString(R.string.status_loading_more), showProgress);
+ updateFooter(mContext.getString(R.string.status_loading_more), showProgress);
} else {
String message;
if (!mCurrentFolder.lastCheckFailed) {
if (mAccount.getDisplayCount() == 0) {
- message = getString(R.string.message_list_load_more_messages_action);
+ message = mContext.getString(R.string.message_list_load_more_messages_action);
} else {
- message = String.format(getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount());
+ message = String.format(mContext.getString(R.string.load_more_messages_fmt), mAccount.getDisplayCount());
}
} else {
- message = getString(R.string.status_loading_more_failed);
+ message = mContext.getString(R.string.status_loading_more_failed);
}
final boolean showProgress = false;
updateFooter(message, showProgress);
@@ -2205,6 +1842,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
public void updateFooter(final String text, final boolean progressVisible) {
+ if (mFooterView == null) {
+ return;
+ }
+
FooterViewHolder holder = (FooterViewHolder) mFooterView.getTag();
holder.progress.setVisibility(progressVisible ? ProgressBar.VISIBLE : ProgressBar.INVISIBLE);
@@ -2223,23 +1864,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public TextView main;
}
- private void setAllSelected(boolean isSelected) {
- mSelectedCount = 0;
-
- for (MessageInfoHolder holder : mAdapter.getMessages()) {
- holder.selected = isSelected;
- mSelectedCount += (isSelected ? 1 : 0);
- }
-
- computeBatchDirection();
- mAdapter.notifyDataSetChanged();
-
- if (isSelected) {
- updateActionModeTitle();
- computeSelectAllVisibility();
- }
- }
-
/**
* Set selection state for all messages.
*
@@ -2248,40 +1872,87 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
* action mode is finished.
*/
private void setSelectionState(boolean selected) {
- mAdapter.setSelectionForAllMesages(selected);
-
if (selected) {
- mSelectedCount = mAdapter.getCount();
- mActionMode = getSherlockActivity().startActionMode(mActionModeCallback);
+ if (mAdapter.getCount() == 0) {
+ // Nothing to do if there are no messages
+ return;
+ }
+
+ mSelectedCount = 0;
+ for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
+ Cursor cursor = (Cursor) mAdapter.getItem(i);
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+ mSelected.add(uniqueId);
+
+ if (mThreadedList) {
+ int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
+ mSelectedCount += (threadCount > 1) ? threadCount : 1;
+ } else {
+ mSelectedCount++;
+ }
+ }
+
+ if (mActionMode == null) {
+ mActionMode = getSherlockActivity().startActionMode(mActionModeCallback);
+ }
+ computeBatchDirection();
updateActionModeTitle();
computeSelectAllVisibility();
- computeBatchDirection();
} else {
+ mSelected.clear();
mSelectedCount = 0;
if (mActionMode != null) {
mActionMode.finish();
+ mActionMode = null;
}
}
+
+ mAdapter.notifyDataSetChanged();
}
- private void toggleMessageSelect(final MessageInfoHolder holder){
+ private void toggleMessageSelect(int listViewPosition) {
+ int adapterPosition = listViewToAdapterPosition(listViewPosition);
+ if (adapterPosition == AdapterView.INVALID_POSITION) {
+ return;
+ }
+
+ toggleMessageSelectWithAdapterPosition(adapterPosition);
+ }
+
+ private void toggleMessageSelectWithAdapterPosition(int adapterPosition) {
+ Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+
+ boolean selected = mSelected.contains(uniqueId);
+ if (!selected) {
+ mSelected.add(uniqueId);
+ } else {
+ mSelected.remove(uniqueId);
+ }
+
+ int selectedCountDelta = 1;
+ if (mThreadedList) {
+ int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
+ if (threadCount > 1) {
+ selectedCountDelta = threadCount;
+ }
+ }
+
if (mActionMode != null) {
- if (mSelectedCount == 1 && holder.selected) {
+ if (mSelectedCount == selectedCountDelta && selected) {
mActionMode.finish();
+ mActionMode = null;
return;
}
} else {
mActionMode = getSherlockActivity().startActionMode(mActionModeCallback);
}
- if (holder.selected) {
- holder.selected = false;
- mSelectedCount -= 1;
+ if (selected) {
+ mSelectedCount -= selectedCountDelta;
} else {
- holder.selected = true;
- mSelectedCount += 1;
+ mSelectedCount += selectedCountDelta;
}
- mAdapter.notifyDataSetChanged();
computeBatchDirection();
updateActionModeTitle();
@@ -2290,6 +1961,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mActionMode.invalidate();
computeSelectAllVisibility();
+
+ mAdapter.notifyDataSetChanged();
}
private void updateActionModeTitle() {
@@ -2297,19 +1970,24 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
private void computeSelectAllVisibility() {
- mActionModeCallback.showSelectAll(mSelectedCount != mAdapter.getCount());
+ mActionModeCallback.showSelectAll(mSelected.size() != mAdapter.getCount());
}
private void computeBatchDirection() {
boolean isBatchFlag = false;
boolean isBatchRead = false;
- for (MessageInfoHolder holder : mAdapter.getMessages()) {
- if (holder.selected) {
- if (!holder.flagged) {
+ for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
+ Cursor cursor = (Cursor) mAdapter.getItem(i);
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+
+ if (mSelected.contains(uniqueId)) {
+ String flags = cursor.getString(FLAGS_COLUMN);
+
+ if (!flags.contains(Flag.FLAGGED.name())) {
isBatchFlag = true;
}
- if (!holder.read) {
+ if (!flags.contains(Flag.SEEN.name())) {
isBatchRead = true;
}
@@ -2323,132 +2001,154 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mActionModeCallback.showFlag(isBatchFlag);
}
- /**
- * @param holders
- * Messages to update. Never {@code null}.
- * @param flag
- * Flag to be updated on the specified messages. Never
- * {@code null}.
- * @param newState
- * State to set for the given flag.
- */
- private void setFlag(final List holders, final Flag flag, final boolean newState) {
- if (holders.isEmpty()) {
+ private void setFlag(Message message, final Flag flag, final boolean newState) {
+ setFlag(Collections.singletonList(message), flag, newState);
+ }
+
+ private void setFlag(List messages, final Flag flag, final boolean newState) {
+ if (messages.size() == 0) {
return;
}
- final Message[] messageList = new Message[holders.size()];
- int i = 0;
- for (final Iterator iterator = holders.iterator(); iterator.hasNext(); i++) {
- final MessageInfoHolder holder = iterator.next();
- messageList[i] = holder.message;
- if (flag == Flag.SEEN) {
- holder.read = newState;
- } else if (flag == Flag.FLAGGED) {
- holder.flagged = newState;
- }
+
+ if (mThreadedList) {
+ mController.setFlagForThreads(messages, flag, newState);
+ } else {
+ mController.setFlag(messages, flag, newState);
}
- mController.setFlag(messageList, flag, newState);
- mAdapter.sortMessages();
computeBatchDirection();
}
+ private void onMove(Message message) {
+ onMove(Collections.singletonList(message));
+ }
+
/**
* Display the message move activity.
*
- * @param holders
- * Never {@code null}.
+ * @param messages
+ * Never {@code null}.
*/
- private void onMove(final List holders) {
- if (!checkCopyOrMovePossible(holders, FolderOperation.MOVE)) {
+ private void onMove(List messages) {
+ if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) {
return;
}
- final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder;
- displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, holders);
+ final Folder folder = (messages.size() == 1) ?
+ messages.get(0).getFolder() : mCurrentFolder.folder;
+
+ displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, messages);
+ }
+
+ private void onCopy(Message message) {
+ onCopy(Collections.singletonList(message));
}
/**
* Display the message copy activity.
*
- * @param holders
- * Never {@code null}.
+ * @param messages
+ * Never {@code null}.
*/
- private void onCopy(final List holders) {
- if (!checkCopyOrMovePossible(holders, FolderOperation.COPY)) {
+ private void onCopy(List messages) {
+ if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) {
return;
}
- final Folder folder = holders.size() == 1 ? holders.get(0).message.getFolder() : mCurrentFolder.folder;
- displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, holders);
+ final Folder folder = (messages.size() == 1) ?
+ messages.get(0).getFolder() : mCurrentFolder.folder;
+
+ displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, messages);
}
/**
- * Helper method to manage the invocation of
- * {@link #startActivityForResult(Intent, int)} for a folder operation
- * ({@link ChooseFolder} activity), while saving a list of associated
- * messages.
+ * Helper method to manage the invocation of {@link #startActivityForResult(Intent, int)} for a
+ * folder operation ({@link ChooseFolder} activity), while saving a list of associated messages.
*
* @param requestCode
- * If >= 0, this code will be returned in onActivityResult() when
- * the activity exits.
+ * If {@code >= 0}, this code will be returned in {@code onActivityResult()} when the
+ * activity exits.
* @param folder
- * Never {@code null}.
- * @param holders
- * Messages to be affected by the folder operation. Never
- * {@code null}.
+ * The source folder. Never {@code null}.
+ * @param messages
+ * Messages to be affected by the folder operation. Never {@code null}.
+ *
* @see #startActivityForResult(Intent, int)
*/
- private void displayFolderChoice(final int requestCode, final Folder folder, final List holders) {
+ private void displayFolderChoice(final int requestCode, final Folder folder, final List messages) {
final Intent intent = new Intent(getActivity(), ChooseFolder.class);
intent.putExtra(ChooseFolder.EXTRA_ACCOUNT, folder.getAccount().getUuid());
intent.putExtra(ChooseFolder.EXTRA_CUR_FOLDER, folder.getName());
intent.putExtra(ChooseFolder.EXTRA_SEL_FOLDER, folder.getAccount().getLastSelectedFolderName());
// remember the selected messages for #onActivityResult
- mActiveMessages = holders;
+ mActiveMessages = messages;
startActivityForResult(intent, requestCode);
}
- /**
- * @param holders
- * Never {@code null}.
- */
- private void onArchive(final List holders) {
- final String folderName = holders.get(0).message.getFolder().getAccount().getArchiveFolderName();
- if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) {
- return;
+ private void onArchive(final Message message) {
+ onArchive(Collections.singletonList(message));
+ }
+
+ private void onArchive(final List messages) {
+ Map> messagesByAccount = groupMessagesByAccount(messages);
+
+ for (Entry> entry : messagesByAccount.entrySet()) {
+ Account account = entry.getKey();
+ String archiveFolder = account.getArchiveFolderName();
+
+ if (!K9.FOLDER_NONE.equals(archiveFolder)) {
+ move(entry.getValue(), archiveFolder);
+ }
}
- // TODO one should separate messages by account and call move afterwards
- // (because each account might have a specific Archive folder name)
- move(holders, folderName);
+ }
+
+ private Map> groupMessagesByAccount(final List messages) {
+ Map> messagesByAccount = new HashMap>();
+ for (Message message : messages) {
+ Account account = message.getFolder().getAccount();
+
+ List msgList = messagesByAccount.get(account);
+ if (msgList == null) {
+ msgList = new ArrayList();
+ messagesByAccount.put(account, msgList);
+ }
+
+ msgList.add(message);
+ }
+ return messagesByAccount;
+ }
+
+ private void onSpam(Message message) {
+ onSpam(Collections.singletonList(message));
}
/**
- * @param holders
- * Never {@code null}.
+ * Move messages to the spam folder.
+ *
+ * @param messages
+ * The messages to move to the spam folder. Never {@code null}.
*/
- private void onSpam(final List holders) {
+ private void onSpam(List messages) {
if (K9.confirmSpam()) {
// remember the message selection for #onCreateDialog(int)
- mActiveMessages = holders;
+ mActiveMessages = messages;
showDialog(R.id.dialog_confirm_spam);
} else {
- onSpamConfirmed(holders);
+ onSpamConfirmed(messages);
}
}
- /**
- * @param holders
- * Never {@code null}.
- */
- private void onSpamConfirmed(final List holders) {
- final String folderName = holders.get(0).message.getFolder().getAccount().getSpamFolderName();
- if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) {
- return;
+ private void onSpamConfirmed(List messages) {
+ Map> messagesByAccount = groupMessagesByAccount(messages);
+
+ for (Entry> entry : messagesByAccount.entrySet()) {
+ Account account = entry.getKey();
+ String spamFolder = account.getSpamFolderName();
+
+ if (!K9.FOLDER_NONE.equals(spamFolder)) {
+ move(entry.getValue(), spamFolder);
+ }
}
- // TODO one should separate messages by account and call move afterwards
- // (because each account might have a specific Spam folder name)
- move(holders, folderName);
}
private static enum FolderOperation {
@@ -2456,32 +2156,36 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
/**
- * Display an Toast message if any message isn't synchronized
+ * Display a Toast message if any message isn't synchronized
*
- * @param holders
- * Never null
.
+ * @param messages
+ * The messages to copy or move. Never {@code null}.
* @param operation
- * Never {@code null}.
+ * The type of operation to perform. Never {@code null}.
*
- * @return true
if operation is possible
+ * @return {@code true}, if operation is possible.
*/
- private boolean checkCopyOrMovePossible(final List holders, final FolderOperation operation) {
- if (holders.isEmpty()) {
+ private boolean checkCopyOrMovePossible(final List messages,
+ final FolderOperation operation) {
+
+ if (messages.size() == 0) {
return false;
}
+
boolean first = true;
- for (final MessageInfoHolder holder : holders) {
- final Message message = holder.message;
+ for (final Message message : messages) {
if (first) {
first = false;
// account check
final Account account = message.getFolder().getAccount();
- if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) {
+ if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) ||
+ (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) {
return false;
}
}
// message check
- if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) {
+ if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) ||
+ (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) {
final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
Toast.LENGTH_LONG);
toast.show();
@@ -2491,52 +2195,28 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
return true;
}
- /**
- * Helper method to get a List of message ready to be processed. This implementation will return a list containing the sole argument.
- *
- * @param holder Never {@code null}.
- * @return Never {@code null}.
- */
- private List getSelectionFromMessage(final MessageInfoHolder holder) {
- final List selection = Collections.singletonList(holder);
- return selection;
- }
-
- /**
- * Helper method to get a List of message ready to be processed. This implementation will iterate over messages and choose the checked ones.
- *
- * @return Never {@code null}.
- */
- private List getSelectionFromCheckboxes() {
- final List selection = new ArrayList();
-
- for (final MessageInfoHolder holder : mAdapter.getMessages()) {
- if (holder.selected) {
- selection.add(holder);
- }
- }
-
- return selection;
- }
-
/**
* Copy the specified messages to the specified folder.
*
- * @param holders Never {@code null}.
- * @param destination Never {@code null}.
+ * @param messages
+ * List of messages to copy. Never {@code null}.
+ * @param destination
+ * The name of the destination folder. Never {@code null}.
*/
- private void copy(final List holders, final String destination) {
- copyOrMove(holders, destination, FolderOperation.COPY);
+ private void copy(List messages, final String destination) {
+ copyOrMove(messages, destination, FolderOperation.COPY);
}
/**
* Move the specified messages to the specified folder.
*
- * @param holders Never {@code null}.
- * @param destination Never {@code null}.
+ * @param messages
+ * The list of messages to move. Never {@code null}.
+ * @param destination
+ * The name of the destination folder. Never {@code null}.
*/
- private void move(final List holders, final String destination) {
- copyOrMove(holders, destination, FolderOperation.MOVE);
+ private void move(List messages, final String destination) {
+ copyOrMove(messages, destination, FolderOperation.MOVE);
}
/**
@@ -2544,16 +2224,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
* {@link #move(List, String)}. This method was added mainly because those 2
* methods share common behavior.
*
- * Note: Must be called from the UI thread!
- *
- * @param holders
- * Never {@code null}.
+ * @param messages
+ * The list of messages to copy or move. Never {@code null}.
* @param destination
- * Never {@code null}.
+ * The name of the destination folder. Never {@code null}.
* @param operation
- * Never {@code null}.
+ * Specifies what operation to perform. Never {@code null}.
*/
- private void copyOrMove(final List holders, final String destination, final FolderOperation operation) {
+ private void copyOrMove(List messages, final String destination,
+ final FolderOperation operation) {
+
if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) {
return;
}
@@ -2562,68 +2242,59 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
Account account = null;
String folderName = null;
- final List messages = new ArrayList(holders.size());
+ List outMessages = new ArrayList();
- for (final MessageInfoHolder holder : holders) {
- final Message message = holder.message;
+ for (Message message : messages) {
if (first) {
first = false;
+
folderName = message.getFolder().getName();
account = message.getFolder().getAccount();
- if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(account))) {
- // account is not copy/move capable
+
+ if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(account)) ||
+ (operation == FolderOperation.COPY &&
+ !mController.isCopyCapable(account))) {
+
+ // Account is not copy/move capable
return;
}
- } else if (!account.equals(message.getFolder().getAccount())
- || !folderName.equals(message.getFolder().getName())) {
- // make sure all messages come from the same account/folder?
+ } else if (!message.getFolder().getAccount().equals(account) ||
+ !message.getFolder().getName().equals(folderName)) {
+
+ // Make sure all messages come from the same account/folder
return;
}
- if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) || (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) {
- final Toast toast = Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
- Toast.LENGTH_LONG);
- toast.show();
+
+ if ((operation == FolderOperation.MOVE && !mController.isMoveCapable(message)) ||
+ (operation == FolderOperation.COPY && !mController.isCopyCapable(message))) {
+
+ Toast.makeText(getActivity(), R.string.move_copy_cannot_copy_unsynced_message,
+ Toast.LENGTH_LONG).show();
// XXX return meaningful error value?
// message isn't synchronized
return;
}
- messages.add(message);
+
+ outMessages.add(message);
}
if (operation == FolderOperation.MOVE) {
- mController.moveMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination,
- null);
- mAdapter.removeMessages(holders);
+ if (mThreadedList) {
+ mController.moveMessagesInThread(account, folderName, outMessages, destination);
+ } else {
+ mController.moveMessages(account, folderName, outMessages, destination, null);
+ }
} else {
- mController.copyMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination,
- null);
+ if (mThreadedList) {
+ mController.copyMessagesInThread(account, folderName, outMessages, destination);
+ } else {
+ mController.copyMessages(account, folderName, outMessages, destination, null);
+ }
}
}
- /**
- * Return the currently "open" account if available.
- *
- * @param prefs
- * A {@link Preferences} instance that might be used to retrieve the current
- * {@link Account}.
- *
- * @return The {@code Account} all displayed messages belong to.
- */
- private Account getCurrentAccount(Preferences prefs) {
- Account account = null;
- if (mQueryString != null && !mIntegrate && mAccountUuids != null &&
- mAccountUuids.length == 1) {
- String uuid = mAccountUuids[0];
- account = prefs.getAccount(uuid);
- } else if (mAccount != null) {
- account = mAccount;
- }
-
- return account;
- }
-
class ActionModeCallback implements ActionMode.Callback {
private MenuItem mSelectAll;
@@ -2640,30 +2311,51 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mFlag = menu.findItem(R.id.flag);
mUnflag = menu.findItem(R.id.unflag);
- if (mQueryString != null) {
+ // we don't support cross account actions atm
+ if (!mSingleAccountMode) {
// show all
menu.findItem(R.id.move).setVisible(true);
menu.findItem(R.id.archive).setVisible(true);
menu.findItem(R.id.spam).setVisible(true);
menu.findItem(R.id.copy).setVisible(true);
- // hide uncapable
- /*
- * TODO think of a better way then looping over all
- * messages.
- */
- final List selection = getSelectionFromCheckboxes();
- Account account;
+ Set accountUuids = getAccountUuidsForSelected();
- for (MessageInfoHolder holder : selection) {
- account = holder.message.getFolder().getAccount();
- setContextCapabilities(account, menu);
+ for (String accountUuid : accountUuids) {
+ Account account = mPreferences.getAccount(accountUuid);
+ if (account != null) {
+ setContextCapabilities(account, menu);
+ }
}
}
return true;
}
+ /**
+ * Get the set of account UUIDs for the selected messages.
+ */
+ private Set getAccountUuidsForSelected() {
+ int maxAccounts = mAccountUuids.length;
+ Set accountUuids = new HashSet(maxAccounts);
+
+ for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
+ Cursor cursor = (Cursor) mAdapter.getItem(position);
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+
+ if (mSelected.contains(uniqueId)) {
+ String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
+ accountUuids.add(accountUuid);
+
+ if (accountUuids.size() == mAccountUuids.length) {
+ break;
+ }
+ }
+ }
+
+ return accountUuids;
+ }
+
@Override
public void onDestroyActionMode(ActionMode mode) {
mActionMode = null;
@@ -2672,7 +2364,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
mMarkAsUnread = null;
mFlag = null;
mUnflag = null;
- setAllSelected(false);
+ setSelectionState(false);
}
@Override
@@ -2681,53 +2373,49 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
inflater.inflate(R.menu.message_list_context, menu);
// check capabilities
- if (mQueryString == null) {
- setContextCapabilities(mAccount, menu);
- }
+ setContextCapabilities(mAccount, menu);
return true;
}
/**
- * Disables menu options based on if the account supports it or not.
- * It also checks the controller and for now the 'mode' the messagelist
- * is operation in ( query or not ).
+ * Disables menu options not supported by the account type or current "search view".
*
- * @param mAccount Account to check capabilities of.
- * @param menu Menu to adapt.
+ * @param account
+ * The account to query for its capabilities.
+ * @param menu
+ * The menu to adapt.
*/
- private void setContextCapabilities(Account mAccount, Menu menu) {
- /*
- * TODO get rid of this when we finally split the messagelist into
- * a folder content display and a search result display
- */
- if (mQueryString != null) {
+ private void setContextCapabilities(Account account, Menu menu) {
+ if (!mSingleAccountMode) {
+ // We don't support cross-account copy/move operations right now
menu.findItem(R.id.move).setVisible(false);
menu.findItem(R.id.copy).setVisible(false);
+ //TODO: we could support the archive and spam operations if all selected messages
+ // belong to non-POP3 accounts
menu.findItem(R.id.archive).setVisible(false);
menu.findItem(R.id.spam).setVisible(false);
- return;
- }
+ } else {
+ // hide unsupported
+ if (!mController.isCopyCapable(account)) {
+ menu.findItem(R.id.copy).setVisible(false);
+ }
- // hide unsupported
- if (!mController.isCopyCapable(mAccount)) {
- menu.findItem(R.id.copy).setVisible(false);
- }
+ if (!mController.isMoveCapable(account)) {
+ menu.findItem(R.id.move).setVisible(false);
+ menu.findItem(R.id.archive).setVisible(false);
+ menu.findItem(R.id.spam).setVisible(false);
+ }
- if (!mController.isMoveCapable(mAccount)) {
- menu.findItem(R.id.move).setVisible(false);
- menu.findItem(R.id.archive).setVisible(false);
- menu.findItem(R.id.spam).setVisible(false);
- }
+ if (!account.hasArchiveFolder()) {
+ menu.findItem(R.id.archive).setVisible(false);
+ }
- if (!mAccount.hasArchiveFolder()) {
- menu.findItem(R.id.archive).setVisible(false);
- }
-
- if (!mAccount.hasSpamFolder()) {
- menu.findItem(R.id.spam).setVisible(false);
+ if (!account.hasSpamFolder()) {
+ menu.findItem(R.id.spam).setVisible(false);
+ }
}
}
@@ -2753,7 +2441,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- final List selection = getSelectionFromCheckboxes();
+ List messages = getCheckedMessages();
/*
* In the following we assume that we can't move or copy
@@ -2764,49 +2452,51 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
*/
switch (item.getItemId()) {
case R.id.delete: {
- onDelete(selection);
+ onDelete(messages);
+
+ //FIXME
mSelectedCount = 0;
break;
}
case R.id.mark_as_read: {
- setFlag(selection, Flag.SEEN, true);
+ setFlag(messages, Flag.SEEN, true);
break;
}
case R.id.mark_as_unread: {
- setFlag(selection, Flag.SEEN, false);
+ setFlag(messages, Flag.SEEN, false);
break;
}
case R.id.flag: {
- setFlag(selection, Flag.FLAGGED, true);
+ setFlag(messages, Flag.FLAGGED, true);
break;
}
case R.id.unflag: {
- setFlag(selection, Flag.FLAGGED, false);
+ setFlag(messages, Flag.FLAGGED, false);
break;
}
case R.id.select_all: {
- setAllSelected(true);
+ selectAll();
break;
}
// only if the account supports this
case R.id.archive: {
- onArchive(selection);
+ onArchive(messages);
mSelectedCount = 0;
break;
}
case R.id.spam: {
- onSpam(selection);
+ onSpam(messages);
mSelectedCount = 0;
break;
}
case R.id.move: {
- onMove(selection);
+ onMove(messages);
mSelectedCount = 0;
break;
}
case R.id.copy: {
- onCopy(selection);
+ onCopy(messages);
mSelectedCount = 0;
break;
}
@@ -2848,18 +2538,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
public void checkMail() {
- mController.synchronizeMailbox(mAccount, mFolderName, mAdapter.mListener, null);
- mController.sendPendingMessages(mAccount, mAdapter.mListener);
+ mController.synchronizeMailbox(mAccount, mFolderName, mListener, null);
+ mController.sendPendingMessages(mAccount, mListener);
}
/**
- * We need to do some special clean up when leaving a remote search result screen. If no remote search is
- * in progress, this method does nothing special.
+ * We need to do some special clean up when leaving a remote search result screen. If no
+ * remote search is in progress, this method does nothing special.
*/
@Override
public void onStop() {
// If we represent a remote search, then kill that before going back.
- if (mSearchAccount != null && mSearchFolder != null && mRemoteSearchFuture != null) {
+ if (isRemoteSearch() && mRemoteSearchFuture != null) {
try {
Log.i(K9.LOG_TAG, "Remote search in progress, attempting to abort...");
// Canceling the future stops any message fetches in progress.
@@ -2868,13 +2558,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
Log.e(K9.LOG_TAG, "Could not cancel remote search future.");
}
// Closing the folder will kill off the connection if we're mid-search.
- Context appContext = getActivity().getApplicationContext();
- final Account searchAccount = Preferences.getPreferences(appContext).getAccount(mSearchAccount);
- final Store remoteStore = searchAccount.getRemoteStore();
- final Folder remoteFolder = remoteStore.getFolder(mSearchFolder);
+ final Account searchAccount = mAccount;
+ final Folder remoteFolder = mCurrentFolder.folder;
remoteFolder.close();
// Send a remoteSearchFinished() message for good measure.
- mAdapter.mListener.remoteSearchFinished(searchAccount, mSearchFolder, 0, null);
+ //mAdapter.mListener.remoteSearchFinished(searchAccount, mCurrentFolder.name, 0, null);
} catch (Exception e) {
// Since the user is going back, log and squash any exceptions.
Log.e(K9.LOG_TAG, "Could not abort remote search before going back", e);
@@ -2886,8 +2574,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public ArrayList getMessageReferences() {
ArrayList messageRefs = new ArrayList();
- for (MessageInfoHolder holder : mAdapter.getMessages()) {
- MessageReference ref = holder.message.makeMessageReference();
+ for (int i = 0, len = mAdapter.getCount(); i < len; i++) {
+ Cursor cursor = (Cursor) mAdapter.getItem(i);
+
+ MessageReference ref = new MessageReference();
+ ref.accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
+ ref.folderName = cursor.getString(FOLDER_NAME_COLUMN);
+ ref.uid = cursor.getString(UID_COLUMN);
+
messageRefs.add(ref);
}
@@ -2921,7 +2615,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public interface MessageListFragmentListener {
void setMessageListProgress(int level);
- void remoteSearch(String searchAccount, String searchFolder, String queryString);
+ void showThread(Account account, String folderName, long rootId);
void showMoreFromSameSender(String senderAddress);
void onResendMessage(Message message);
void onForward(Message message);
@@ -2933,69 +2627,101 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
void setUnreadCount(int unread);
void onCompose(Account account);
boolean startSearch(Account account, String folderName);
+ void remoteSearchStarted();
}
public void onReverseSort() {
changeSort(mSortType);
}
- private MessageInfoHolder getSelection() {
- return (MessageInfoHolder) mListView.getSelectedItem();
+ private Message getSelectedMessage() {
+ int listViewPosition = mListView.getSelectedItemPosition();
+ int adapterPosition = listViewToAdapterPosition(listViewPosition);
+
+ return getMessageAtPosition(adapterPosition);
+ }
+
+ private Message getMessageAtPosition(int adapterPosition) {
+ if (adapterPosition == AdapterView.INVALID_POSITION) {
+ return null;
+ }
+
+ Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
+ String uid = cursor.getString(UID_COLUMN);
+
+ Account account = getAccountFromCursor(cursor);
+ long folderId = cursor.getLong(FOLDER_ID_COLUMN);
+ Folder folder = getFolderById(account, folderId);
+
+ try {
+ return folder.getMessage(uid);
+ } catch (MessagingException e) {
+ Log.e(K9.LOG_TAG, "Something went wrong while fetching a message", e);
+ }
+
+ return null;
+ }
+
+ private List getCheckedMessages() {
+ List messages = new ArrayList(mSelected.size());
+ for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
+ Cursor cursor = (Cursor) mAdapter.getItem(position);
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+
+ if (mSelected.contains(uniqueId)) {
+ messages.add(getMessageAtPosition(position));
+ }
+ }
+
+ return messages;
}
public void onDelete() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
onDelete(Collections.singletonList(message));
}
}
public void toggleMessageSelect() {
- MessageInfoHolder message = getSelection();
- if (message != null) {
- toggleMessageSelect(message);
- }
+ toggleMessageSelect(mListView.getSelectedItemPosition());
}
public void onToggleFlag() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
- setFlag(Collections.singletonList(message), Flag.FLAGGED, !message.flagged);
+ setFlag(message, Flag.FLAGGED, !message.isSet(Flag.FLAGGED));
}
}
public void onMove() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
- onMove(Collections.singletonList(message));
+ onMove(message);
}
}
public void onArchive() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
- onArchive(Collections.singletonList(message));
+ onArchive(message);
}
}
public void onCopy() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
- onCopy(Collections.singletonList(message));
+ onCopy(message);
}
}
public void onToggleRead() {
- MessageInfoHolder message = getSelection();
+ Message message = getSelectedMessage();
if (message != null) {
- setFlag(Collections.singletonList(message), Flag.SEEN, !message.read);
+ setFlag(message, Flag.SEEN, !message.isSet(Flag.SEEN));
}
}
- public boolean isSearchQuery() {
- return (mQueryString != null || mIntegrate);
- }
-
public boolean isOutbox() {
return (mFolderName != null && mFolderName.equals(mAccount.getOutboxFolderName()));
}
@@ -3005,7 +2731,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
public boolean isRemoteFolder() {
- if (isSearchQuery() || isOutbox() || isErrorFolder()) {
+ if (mSearch.isManualSearch() || isOutbox() || isErrorFolder()) {
return false;
}
@@ -3017,6 +2743,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
return true;
}
+ public boolean isManualSearch() {
+ return mSearch.isManualSearch();
+ }
+
public boolean isAccountExpungeCapable() {
try {
return (mAccount != null && mAccount.getRemoteStore().isExpungeCapable());
@@ -3028,7 +2758,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
public void onRemoteSearch() {
// Remote search is useless without the network.
if (mHasConnectivity) {
- onRemoteSearchRequested(true);
+ onRemoteSearchRequested();
} else {
Toast.makeText(getActivity(), getText(R.string.remote_search_unavailable_no_network),
Toast.LENGTH_SHORT).show();
@@ -3036,19 +2766,16 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
}
public boolean isRemoteSearch() {
- return mRemoteSearch;
+ return mRemoteSearchPerformed;
}
public boolean isRemoteSearchAllowed() {
- if (!isSearchQuery() || mRemoteSearch || mSearchFolder == null || mSearchAccount == null) {
+ if (!mSearch.isManualSearch() || mRemoteSearchPerformed || !mSingleFolderMode) {
return false;
}
- Context appContext = getActivity().getApplicationContext();
- final Preferences prefs = Preferences.getPreferences(appContext);
-
boolean allowRemoteSearch = false;
- final Account searchAccount = prefs.getAccount(mSearchAccount);
+ final Account searchAccount = mAccount;
if (searchAccount != null) {
allowRemoteSearch = searchAccount.allowRemoteSearch();
}
@@ -3060,4 +2787,127 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
String folderName = (mCurrentFolder != null) ? mCurrentFolder.name : null;
return mFragmentListener.startSearch(mAccount, folderName);
}
+
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ String accountUuid = mAccountUuids[id];
+ Account account = mPreferences.getAccount(accountUuid);
+
+ Uri uri;
+ String[] projection;
+ if (mThreadedList) {
+ uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded");
+ projection = THREADED_PROJECTION;
+ } else {
+ uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
+ projection = PROJECTION;
+ }
+
+ StringBuilder query = new StringBuilder();
+ List queryArgs = new ArrayList();
+ SqlQueryBuilder.buildWhereClause(account, mSearch.getConditions(), query, queryArgs);
+
+ String selection = query.toString();
+ String[] selectionArgs = queryArgs.toArray(new String[0]);
+
+ String sortOrder = buildSortOrder();
+
+ return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs,
+ sortOrder);
+ }
+
+ private String buildSortOrder() {
+ String sortColumn = MessageColumns.ID;
+ switch (mSortType) {
+ case SORT_ARRIVAL: {
+ sortColumn = MessageColumns.INTERNAL_DATE;
+ break;
+ }
+ case SORT_ATTACHMENT: {
+ sortColumn = "(" + MessageColumns.ATTACHMENT_COUNT + " < 1)";
+ break;
+ }
+ case SORT_FLAGGED: {
+ sortColumn = "(" + MessageColumns.FLAGS + " NOT LIKE '%FLAGGED%')";
+ break;
+ }
+// case SORT_SENDER: {
+// //FIXME
+// sortColumn = MessageColumns.SENDER_LIST;
+// break;
+// }
+ case SORT_SUBJECT: {
+ sortColumn = MessageColumns.SUBJECT + " COLLATE NOCASE";
+ break;
+ }
+ case SORT_UNREAD: {
+ sortColumn = "(" + MessageColumns.FLAGS + " LIKE '%SEEN%')";
+ break;
+ }
+ case SORT_DATE:
+ default: {
+ sortColumn = MessageColumns.DATE;
+ }
+ }
+
+ String sortDirection = (mSortAscending) ? " ASC" : " DESC";
+ String secondarySort;
+ if (mSortType == SortType.SORT_DATE || mSortType == SortType.SORT_ARRIVAL) {
+ secondarySort = "";
+ } else {
+ secondarySort = MessageColumns.DATE + ((mSortDateAscending) ? " ASC, " : " DESC, ");
+ }
+
+ String sortOrder = sortColumn + sortDirection + ", " + secondarySort +
+ MessageColumns.ID + " DESC";
+ return sortOrder;
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor data) {
+ Cursor cursor;
+ if (mCursors.length > 1) {
+ mCursors[loader.getId()] = data;
+ cursor = new MergeCursorWithUniqueId(mCursors, getComparator());
+ mUniqueIdColumn = cursor.getColumnIndex("_id");
+ } else {
+ cursor = data;
+ mUniqueIdColumn = ID_COLUMN;
+ }
+
+ cleanupSelected(cursor);
+
+ mAdapter.swapCursor(cursor);
+ }
+
+ private void cleanupSelected(Cursor cursor) {
+ if (mSelected.size() == 0) {
+ return;
+ }
+
+ Set selected = new HashSet();
+ for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
+ long uniqueId = cursor.getLong(mUniqueIdColumn);
+ if (mSelected.contains(uniqueId)) {
+ selected.add(uniqueId);
+ }
+ }
+
+ mSelected = selected;
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ mSelected = null;
+ mAdapter.swapCursor(null);
+ }
+
+ private Account getAccountFromCursor(Cursor cursor) {
+ String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
+ return mPreferences.getAccount(accountUuid);
+ }
+
+ private void remoteSearchFinished() {
+ mRemoteSearchFuture = null;
+ }
}
diff --git a/src/com/fsck/k9/fragment/MessageViewFragment.java b/src/com/fsck/k9/fragment/MessageViewFragment.java
index cc487042c..43681e756 100644
--- a/src/com/fsck/k9/fragment/MessageViewFragment.java
+++ b/src/com/fsck/k9/fragment/MessageViewFragment.java
@@ -1,6 +1,7 @@
package com.fsck.k9.fragment;
import java.io.File;
+import java.util.Collections;
import android.app.Activity;
import android.content.Context;
@@ -274,7 +275,7 @@ public class MessageViewFragment extends SherlockFragment implements OnClickList
mMenu.findItem(R.id.delete).setEnabled(false);
Message messageToDelete = mMessage;
mFragmentListener.showNextMessageOrReturn();
- mController.deleteMessages(new Message[] {messageToDelete}, null);
+ mController.deleteMessages(Collections.singletonList(messageToDelete), null);
}
}
diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java
new file mode 100644
index 000000000..3325a6e36
--- /dev/null
+++ b/src/com/fsck/k9/helper/MergeCursor.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2012 The K-9 Dog Walkers
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.fsck.k9.helper;
+
+import java.util.Comparator;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.database.CharArrayBuffer;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.net.Uri;
+import android.os.Bundle;
+
+
+/**
+ * This class can be used to combine multiple {@link Cursor}s into one.
+ */
+public class MergeCursor implements Cursor {
+ /**
+ * List of the cursors combined in this object.
+ */
+ protected final Cursor[] mCursors;
+
+ /**
+ * The currently active cursor.
+ */
+ protected Cursor mActiveCursor;
+
+ /**
+ * The index of the currently active cursor in {@link #mCursors}.
+ *
+ * @see #mActiveCursor
+ */
+ protected int mActiveCursorIndex;
+
+ /**
+ * The cursor's current position.
+ */
+ protected int mPosition;
+
+ /**
+ * Used to cache the value of {@link #getCount()}.
+ */
+ private int mCount = -1;
+
+ /**
+ * The comparator that is used to decide how the individual cursors are merged.
+ */
+ private final Comparator mComparator;
+
+
+ /**
+ * Constructor
+ *
+ * @param cursors
+ * The list of cursors this {@code MultiCursor} should combine.
+ * @param comparator
+ * A comparator that is used to decide in what order the individual cursors are merged.
+ */
+ public MergeCursor(Cursor[] cursors, Comparator comparator) {
+ mCursors = cursors.clone();
+ mComparator = comparator;
+
+ resetCursors();
+ }
+
+ private void resetCursors() {
+ mActiveCursorIndex = -1;
+ mActiveCursor = null;
+ mPosition = -1;
+
+ for (int i = 0, len = mCursors.length; i < len; i++) {
+ Cursor cursor = mCursors[i];
+ if (cursor != null) {
+ cursor.moveToPosition(-1);
+
+ if (mActiveCursor == null) {
+ mActiveCursorIndex = i;
+ mActiveCursor = mCursors[mActiveCursorIndex];
+ }
+ }
+ }
+ }
+
+ @Override
+ public void close() {
+ for (Cursor cursor : mCursors) {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+ mActiveCursor.copyStringToBuffer(columnIndex, buffer);
+ }
+
+ @Override
+ public void deactivate() {
+ for (Cursor cursor : mCursors) {
+ if (cursor != null) {
+ cursor.deactivate();
+ }
+ }
+ }
+
+ @Override
+ public byte[] getBlob(int columnIndex) {
+ return mActiveCursor.getBlob(columnIndex);
+ }
+
+ @Override
+ public int getColumnCount() {
+ return mActiveCursor.getColumnCount();
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ return mActiveCursor.getColumnIndex(columnName);
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
+ return mActiveCursor.getColumnIndexOrThrow(columnName);
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mActiveCursor.getColumnName(columnIndex);
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mActiveCursor.getColumnNames();
+ }
+
+ @Override
+ public int getCount() {
+ // CursorLoaders seem to call getCount() a lot. So we're caching the aggregated count.
+ if (mCount == -1) {
+ int count = 0;
+ for (Cursor cursor : mCursors) {
+ if (cursor != null) {
+ count += cursor.getCount();
+ }
+ }
+
+ mCount = count;
+ }
+
+ return mCount;
+ }
+
+ @Override
+ public double getDouble(int columnIndex) {
+ return mActiveCursor.getDouble(columnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex) {
+ return mActiveCursor.getFloat(columnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ return mActiveCursor.getInt(columnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ return mActiveCursor.getLong(columnIndex);
+ }
+
+ @Override
+ public int getPosition() {
+ return mPosition;
+ }
+
+ @Override
+ public short getShort(int columnIndex) {
+ return mActiveCursor.getShort(columnIndex);
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ return mActiveCursor.getString(columnIndex);
+ }
+
+ @TargetApi(11)
+ @Override
+ public int getType(int columnIndex) {
+ return mActiveCursor.getType(columnIndex);
+ }
+
+ @Override
+ public boolean getWantsAllOnMoveCalls() {
+ return mActiveCursor.getWantsAllOnMoveCalls();
+ }
+
+ @Override
+ public boolean isAfterLast() {
+ int count = getCount();
+ if (count == 0) {
+ return true;
+ }
+
+ return (mPosition == count);
+ }
+
+ @Override
+ public boolean isBeforeFirst() {
+ if (getCount() == 0) {
+ return true;
+ }
+
+ return (mPosition == -1);
+ }
+
+ @Override
+ public boolean isClosed() {
+ return mActiveCursor.isClosed();
+ }
+
+ @Override
+ public boolean isFirst() {
+ if (getCount() == 0) {
+ return false;
+ }
+
+ return (mPosition == 0);
+ }
+
+ @Override
+ public boolean isLast() {
+ int count = getCount();
+ if (count == 0) {
+ return false;
+ }
+
+ return (mPosition == (count - 1));
+ }
+
+ @Override
+ public boolean isNull(int columnIndex) {
+ return mActiveCursor.isNull(columnIndex);
+ }
+
+ @Override
+ public boolean move(int offset) {
+ return moveToPosition(mPosition + offset);
+ }
+
+ @Override
+ public boolean moveToFirst() {
+ return moveToPosition(0);
+ }
+
+ @Override
+ public boolean moveToLast() {
+ return moveToPosition(getCount() - 1);
+ }
+
+ @Override
+ public boolean moveToNext() {
+ int count = getCount();
+ if (mPosition == count) {
+ return false;
+ }
+
+ if (mPosition == count - 1) {
+ mActiveCursor.moveToNext();
+ mPosition++;
+ return false;
+ }
+
+ int smallest = -1;
+ for (int i = 0, len = mCursors.length; i < len; i++) {
+ if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) {
+ continue;
+ }
+
+ if (smallest == -1) {
+ smallest = i;
+ mCursors[smallest].moveToNext();
+ continue;
+ }
+
+ Cursor left = mCursors[smallest];
+ Cursor right = mCursors[i];
+
+ right.moveToNext();
+
+ int result = mComparator.compare(left, right);
+ if (result > 0) {
+ smallest = i;
+ left.moveToPrevious();
+ } else {
+ right.moveToPrevious();
+ }
+ }
+
+ mPosition++;
+ if (smallest != -1) {
+ mActiveCursorIndex = smallest;
+ mActiveCursor = mCursors[mActiveCursorIndex];
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean moveToPosition(int position) {
+ // Make sure position isn't past the end of the cursor
+ final int count = getCount();
+ if (position >= count) {
+ mPosition = count;
+ return false;
+ }
+
+ // Make sure position isn't before the beginning of the cursor
+ if (position < 0) {
+ mPosition = -1;
+ return false;
+ }
+
+ // Check for no-op moves, and skip the rest of the work for them
+ if (position == mPosition) {
+ return true;
+ }
+
+ if (position > mPosition) {
+ for (int i = 0, end = position - mPosition; i < end; i++) {
+ if (!moveToNext()) {
+ return false;
+ }
+ }
+ } else {
+ for (int i = 0, end = mPosition - position; i < end; i++) {
+ if (!moveToPrevious()) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean moveToPrevious() {
+ if (mPosition < 0) {
+ return false;
+ }
+
+ mActiveCursor.moveToPrevious();
+
+ if (mPosition == 0) {
+ mPosition = -1;
+ return false;
+ }
+
+ int greatest = -1;
+ for (int i = 0, len = mCursors.length; i < len; i++) {
+ if (mCursors[i] == null || mCursors[i].isBeforeFirst()) {
+ continue;
+ }
+
+ if (greatest == -1) {
+ greatest = i;
+ continue;
+ }
+
+ Cursor left = mCursors[greatest];
+ Cursor right = mCursors[i];
+
+ int result = mComparator.compare(left, right);
+ if (result <= 0) {
+ greatest = i;
+ }
+ }
+
+ mPosition--;
+ if (greatest != -1) {
+ mActiveCursorIndex = greatest;
+ mActiveCursor = mCursors[mActiveCursorIndex];
+ }
+
+ return true;
+ }
+
+ @Override
+ public void registerContentObserver(ContentObserver observer) {
+ for (Cursor cursor : mCursors) {
+ cursor.registerContentObserver(observer);
+ }
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+ for (Cursor cursor : mCursors) {
+ cursor.registerDataSetObserver(observer);
+ }
+ }
+
+ @Deprecated
+ @Override
+ public boolean requery() {
+ boolean success = true;
+ for (Cursor cursor : mCursors) {
+ success &= cursor.requery();
+ }
+
+ return success;
+ }
+
+ @Override
+ public void setNotificationUri(ContentResolver cr, Uri uri) {
+ for (Cursor cursor : mCursors) {
+ cursor.setNotificationUri(cr, uri);
+ }
+ }
+
+ @Override
+ public void unregisterContentObserver(ContentObserver observer) {
+ for (Cursor cursor : mCursors) {
+ cursor.unregisterContentObserver(observer);
+ }
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+ for (Cursor cursor : mCursors) {
+ cursor.unregisterDataSetObserver(observer);
+ }
+ }
+
+ @Override
+ public Bundle getExtras() {
+ throw new RuntimeException("Not implemented");
+ }
+
+ @Override
+ public Bundle respond(Bundle extras) {
+ throw new RuntimeException("Not implemented");
+ }
+}
diff --git a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java
new file mode 100644
index 000000000..111d49fbb
--- /dev/null
+++ b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java
@@ -0,0 +1,83 @@
+package com.fsck.k9.helper;
+
+import java.util.Comparator;
+
+import android.database.Cursor;
+
+
+public class MergeCursorWithUniqueId extends MergeCursor {
+ private static final int SHIFT = 48;
+ private static final long MAX_ID = (1L << SHIFT) - 1;
+ private static final long MAX_CURSORS = 1L << (63 - SHIFT);
+
+ private int mColumnCount = -1;
+ private int mIdColumnIndex = -1;
+
+
+ public MergeCursorWithUniqueId(Cursor[] cursors, Comparator comparator) {
+ super(cursors, comparator);
+
+ if (cursors.length > MAX_CURSORS) {
+ throw new IllegalArgumentException("This class only supports up to " +
+ MAX_CURSORS + " cursors");
+ }
+ }
+
+ @Override
+ public int getColumnCount() {
+ if (mColumnCount == -1) {
+ mColumnCount = super.getColumnCount();
+ }
+
+ return mColumnCount + 1;
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ if ("_id".equals(columnName)) {
+ return getUniqueIdColumnIndex();
+ }
+
+ return super.getColumnIndexOrThrow(columnName);
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
+ if ("_id".equals(columnName)) {
+ return getUniqueIdColumnIndex();
+ }
+
+ return super.getColumnIndexOrThrow(columnName);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ if (columnIndex == getUniqueIdColumnIndex()) {
+ long id = getPerCursorId();
+ if (id > MAX_ID) {
+ throw new RuntimeException("Sorry, " + this.getClass().getName() +
+ " can only handle '_id' values up to " + SHIFT + " bits.");
+ }
+
+ return (((long) mActiveCursorIndex) << SHIFT) + id;
+ }
+
+ return super.getLong(columnIndex);
+ }
+
+ protected int getUniqueIdColumnIndex() {
+ if (mColumnCount == -1) {
+ mColumnCount = super.getColumnCount();
+ }
+
+ return mColumnCount;
+ }
+
+ protected long getPerCursorId() {
+ if (mIdColumnIndex == -1) {
+ mIdColumnIndex = super.getColumnIndexOrThrow("_id");
+ }
+
+ return super.getLong(mIdColumnIndex);
+ }
+}
diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java
index 9c2abb491..51ab08705 100644
--- a/src/com/fsck/k9/helper/MessageHelper.java
+++ b/src/com/fsck/k9/helper/MessageHelper.java
@@ -105,4 +105,28 @@ public class MessageHelper {
mDateFormat = DateFormatter.getDateFormat(mContext);
mTodayDateFormat = android.text.format.DateFormat.getTimeFormat(mContext);
}
+
+ public CharSequence getDisplayName(Account account, Address[] fromAddrs, Address[] toAddrs) {
+ final Contacts contactHelper = K9.showContactName() ? Contacts.getInstance(mContext) : null;
+
+ CharSequence displayName;
+ if (fromAddrs.length > 0 && account.isAnIdentity(fromAddrs[0])) {
+ CharSequence to = Address.toFriendly(toAddrs, contactHelper);
+ displayName = new SpannableStringBuilder(
+ mContext.getString(R.string.message_to_label)).append(to);
+ } else {
+ displayName = Address.toFriendly(fromAddrs, contactHelper);
+ }
+
+ return displayName;
+ }
+
+ public boolean toMe(Account account, Address[] toAddrs) {
+ for (Address address : toAddrs) {
+ if (account.isAnIdentity(address)) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java
index 113032d34..7d7754998 100644
--- a/src/com/fsck/k9/helper/Utility.java
+++ b/src/com/fsck/k9/helper/Utility.java
@@ -1,11 +1,13 @@
package com.fsck.k9.helper;
+import android.annotation.SuppressLint;
import android.app.Application;
import android.content.Context;
import android.database.Cursor;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
+import android.os.Build;
import android.text.Editable;
import android.util.Log;
import android.widget.EditText;
@@ -18,7 +20,10 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Date;
+import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -666,4 +671,57 @@ public class Utility {
return false;
}
}
+
+ private static final Pattern MESSAGE_ID = Pattern.compile("<" +
+ "(?:" +
+ "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" +
+ "(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" +
+ "|" +
+ "\"(?:[^\\\\\"]|\\\\.)*\"" +
+ ")" +
+ "@" +
+ "(?:" +
+ "[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+" +
+ "(?:\\.[a-zA-Z0-9!#$%&'*+\\-/=?^_`{|}~]+)*" +
+ "|" +
+ "\\[(?:[^\\\\\\]]|\\\\.)*\\]" +
+ ")" +
+ ">");
+
+ public static List extractMessageIds(final String text) {
+ List messageIds = new ArrayList();
+ Matcher matcher = MESSAGE_ID.matcher(text);
+
+ int start = 0;
+ while (matcher.find(start)) {
+ String messageId = text.substring(matcher.start(), matcher.end());
+ messageIds.add(messageId);
+ start = matcher.end();
+ }
+
+ return messageIds;
+ }
+
+ public static String extractMessageId(final String text) {
+ Matcher matcher = MESSAGE_ID.matcher(text);
+
+ if (matcher.find()) {
+ return text.substring(matcher.start(), matcher.end());
+ }
+
+ return null;
+ }
+
+ @SuppressLint("NewApi")
+ public static String[] copyOf(String[] original, int newLength) {
+ if (Build.VERSION.SDK_INT >= 9) {
+ return Arrays.copyOf(original, newLength);
+ }
+
+ String[] newArray = new String[newLength];
+ int copyLength = (original.length >= newLength) ? newLength : original.length;
+ System.arraycopy(original, 0, newArray, 0, copyLength);
+
+ return newArray;
+ }
}
diff --git a/src/com/fsck/k9/mail/Message.java b/src/com/fsck/k9/mail/Message.java
index 48e97d585..3d31f0839 100644
--- a/src/com/fsck/k9/mail/Message.java
+++ b/src/com/fsck/k9/mail/Message.java
@@ -143,10 +143,6 @@ public abstract class Message implements Part, Body {
return getContentType().startsWith(mimeType);
}
- public abstract boolean toMe();
- public abstract boolean ccMe();
- public abstract boolean bccMe();
- public abstract boolean fromMe();
public abstract long getId();
public abstract String getPreview();
@@ -197,8 +193,6 @@ public abstract class Message implements Part, Body {
public void destroy() throws MessagingException {}
- public abstract void saveChanges() throws MessagingException;
-
public abstract void setEncoding(String encoding) throws UnavailableStorageException;
public abstract void setCharset(String charset) throws MessagingException;
@@ -213,11 +207,6 @@ public abstract class Message implements Part, Body {
return mReference;
}
- public boolean equalsReference(MessageReference ref) {
- MessageReference tmpReference = makeMessageReference();
- return tmpReference.equals(ref);
- }
-
public long calculateSize() {
try {
diff --git a/src/com/fsck/k9/mail/internet/MimeMessage.java b/src/com/fsck/k9/mail/internet/MimeMessage.java
index 93c80ba1c..2777420de 100644
--- a/src/com/fsck/k9/mail/internet/MimeMessage.java
+++ b/src/com/fsck/k9/mail/internet/MimeMessage.java
@@ -15,7 +15,6 @@ import java.util.UUID;
import org.apache.james.mime4j.MimeException;
import org.apache.james.mime4j.dom.field.DateTimeField;
-import org.apache.james.mime4j.dom.field.ParsedField;
import org.apache.james.mime4j.field.DefaultFieldParser;
import org.apache.james.mime4j.io.EOLConvertingInputStream;
import org.apache.james.mime4j.parser.ContentHandler;
@@ -23,7 +22,6 @@ import org.apache.james.mime4j.parser.MimeStreamParser;
import org.apache.james.mime4j.stream.BodyDescriptor;
import org.apache.james.mime4j.stream.Field;
import org.apache.james.mime4j.stream.MimeConfig;
-import org.apache.james.mime4j.stream.RawField;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
@@ -345,11 +343,6 @@ public class MimeMessage extends Message {
setHeader("References", references);
}
- @Override
- public void saveChanges() throws MessagingException {
- throw new MessagingException("saveChanges not yet implemented");
- }
-
@Override
public Body getBody() {
return mBody;
@@ -593,22 +586,6 @@ public class MimeMessage extends Message {
return message;
}
- public boolean toMe() {
- return false;
- }
-
- public boolean ccMe() {
- return false;
- }
-
- public boolean bccMe() {
- return false;
- }
-
- public boolean fromMe() {
- return false;
- }
-
public long getId() {
return Long.parseLong(mUid); //or maybe .mMessageId?
}
diff --git a/src/com/fsck/k9/mail/store/ImapStore.java b/src/com/fsck/k9/mail/store/ImapStore.java
index 906cf8b04..39fab2478 100644
--- a/src/com/fsck/k9/mail/store/ImapStore.java
+++ b/src/com/fsck/k9/mail/store/ImapStore.java
@@ -1478,7 +1478,7 @@ public class ImapStore extends Store {
fetchFields.add("INTERNALDATE");
fetchFields.add("RFC822.SIZE");
fetchFields.add("BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc " +
- "reply-to message-id " + K9.IDENTITY_HEADER + ")]");
+ "reply-to message-id references " + K9.IDENTITY_HEADER + ")]");
}
if (fp.contains(FetchProfile.Item.STRUCTURE)) {
fetchFields.add("BODYSTRUCTURE");
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 6e16c1cc7..21f03472f 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -13,7 +13,6 @@ import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
@@ -28,6 +27,7 @@ import java.util.regex.Pattern;
import org.apache.commons.io.IOUtils;
import android.app.Application;
+import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
@@ -47,6 +47,7 @@ import com.fsck.k9.activity.Search;
import com.fsck.k9.controller.MessageRemovalListener;
import com.fsck.k9.controller.MessageRetrievalListener;
import com.fsck.k9.helper.HtmlConverter;
+import com.fsck.k9.helper.StringUtils;
import com.fsck.k9.helper.Utility;
import com.fsck.k9.mail.Address;
import com.fsck.k9.mail.Body;
@@ -71,6 +72,12 @@ import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
import com.fsck.k9.mail.store.StorageManager.StorageProvider;
import com.fsck.k9.provider.AttachmentProvider;
+import com.fsck.k9.provider.EmailProvider;
+import com.fsck.k9.search.LocalSearch;
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.SearchCondition;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+import com.fsck.k9.search.SqlQueryBuilder;
/**
*
@@ -82,27 +89,29 @@ public class LocalStore extends Store implements Serializable {
private static final long serialVersionUID = -5142141896809423072L;
private static final Message[] EMPTY_MESSAGE_ARRAY = new Message[0];
-
- /**
- * Immutable empty {@link String} array
- */
private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
- private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN, Flag.FLAGGED };
+ private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0];
/*
* a String containing the columns getMessages expects to work with
* in the correct order.
*/
static private String GET_MESSAGES_COLS =
- "subject, sender_list, date, uid, flags, id, to_list, cc_list, "
- + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview ";
-
+ "subject, sender_list, date, uid, flags, messages.id, to_list, cc_list, "
+ + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview, thread_root, thread_parent ";
static private String GET_FOLDER_COLS = "id, name, unread_count, visible_limit, last_updated, status, push_state, last_pushed, flagged_count, integrate, top_group, poll_class, push_class, display_class";
+ private static final String[] UID_CHECK_PROJECTION = { "uid" };
- protected static final int DB_VERSION = 43;
+ /**
+ * Number of UIDs to check for existence at once.
+ *
+ * @see LocalFolder#extractNewMessages(List)
+ */
+ private static final int UID_CHECK_BATCH_SIZE = 500;
+
+ protected static final int DB_VERSION = 45;
protected String uUid = null;
@@ -110,6 +119,8 @@ public class LocalStore extends Store implements Serializable {
private LockableDatabase database;
+ private ContentResolver mContentResolver;
+
/**
* local://localhost/path/to/database/uuid.db
* This constructor is only used by {@link Store#getLocalInstance(Account, Application)}
@@ -122,6 +133,7 @@ public class LocalStore extends Store implements Serializable {
database = new LockableDatabase(application, account.getUuid(), new StoreSchemaDefinition());
mApplication = application;
+ mContentResolver = application.getContentResolver();
database.setStorageProviderId(account.getLocalStorageProviderId());
uUid = account.getUuid();
@@ -166,10 +178,31 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("CREATE INDEX IF NOT EXISTS folder_name ON folders (name)");
db.execSQL("DROP TABLE IF EXISTS messages");
- db.execSQL("CREATE TABLE messages (id INTEGER PRIMARY KEY, deleted INTEGER default 0, folder_id INTEGER, uid TEXT, subject TEXT, "
- + "date INTEGER, flags TEXT, sender_list TEXT, to_list TEXT, cc_list TEXT, bcc_list TEXT, reply_to_list TEXT, "
- + "html_content TEXT, text_content TEXT, attachment_count INTEGER, internal_date INTEGER, message_id TEXT, preview TEXT, "
- + "mime_type TEXT)");
+ db.execSQL("CREATE TABLE messages (" +
+ "id INTEGER PRIMARY KEY, " +
+ "deleted INTEGER default 0, " +
+ "folder_id INTEGER, " +
+ "uid TEXT, " +
+ "subject TEXT, " +
+ "date INTEGER, " +
+ "flags TEXT, " +
+ "sender_list TEXT, " +
+ "to_list TEXT, " +
+ "cc_list TEXT, " +
+ "bcc_list TEXT, " +
+ "reply_to_list TEXT, " +
+ "html_content TEXT, " +
+ "text_content TEXT, " +
+ "attachment_count INTEGER, " +
+ "internal_date INTEGER, " +
+ "message_id TEXT, " +
+ "preview TEXT, " +
+ "mime_type TEXT, "+
+ "thread_root INTEGER, " +
+ "thread_parent INTEGER, " +
+ "normalized_subject_hash INTEGER, " +
+ "empty INTEGER" +
+ ")");
db.execSQL("DROP TABLE IF EXISTS headers");
db.execSQL("CREATE TABLE headers (id INTEGER PRIMARY KEY, message_id INTEGER, name TEXT, value TEXT)");
@@ -179,6 +212,16 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("DROP INDEX IF EXISTS msg_folder_id");
db.execSQL("DROP INDEX IF EXISTS msg_folder_id_date");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_folder_id_deleted_date ON messages (folder_id,deleted,internal_date)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_empty");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
+
db.execSQL("DROP TABLE IF EXISTS attachments");
db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
@@ -364,6 +407,34 @@ public class LocalStore extends Store implements Serializable {
Log.e(K9.LOG_TAG, "Error trying to fix the outbox folders", e);
}
}
+ if (db.getVersion() < 44) {
+ try {
+ db.execSQL("ALTER TABLE messages ADD thread_root INTEGER");
+ db.execSQL("ALTER TABLE messages ADD thread_parent INTEGER");
+ db.execSQL("ALTER TABLE messages ADD normalized_subject_hash INTEGER");
+ db.execSQL("ALTER TABLE messages ADD empty INTEGER");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+ }
+ if (db.getVersion() < 45) {
+ try {
+ db.execSQL("DROP INDEX IF EXISTS msg_empty");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (empty)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_root ON messages (thread_root)");
+
+ db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
+ db.execSQL("CREATE INDEX IF NOT EXISTS msg_thread_parent ON messages (thread_parent)");
+ } catch (SQLiteException e) {
+ if (! e.getMessage().startsWith("duplicate column name:")) {
+ throw e;
+ }
+ }
+ }
}
} catch (SQLiteException e) {
Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0");
@@ -623,6 +694,26 @@ public class LocalStore extends Store implements Serializable {
return new LocalFolder(name);
}
+ public LocalFolder getFolderById(long folderId) {
+ return new LocalFolder(folderId);
+ }
+
+ private long getFolderId(final String name) throws MessagingException {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Long doDbWork(final SQLiteDatabase db) {
+ Cursor cursor = null;
+ try {
+ cursor = db.rawQuery("SELECT id FROM folders WHERE name = '" + name + "'", null);
+ cursor.moveToFirst();
+ return cursor.getLong(0);
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+ }
+ });
+ }
+
// TODO this takes about 260-300ms, seems slow.
@Override
public List extends Folder > getPersonalNamespaces(boolean forceListAll) throws MessagingException {
@@ -855,97 +946,54 @@ public class LocalStore extends Store implements Serializable {
return true;
}
- public Message[] searchForMessages(MessageRetrievalListener listener, String[] queryFields, String queryString,
- List folders, Message[] messages, final Flag[] requiredFlags, final Flag[] forbiddenFlags) throws MessagingException {
- List args = new LinkedList();
-
- StringBuilder whereClause = new StringBuilder();
- if (queryString != null && queryString.length() > 0) {
- boolean anyAdded = false;
- String likeString = "%" + queryString + "%";
- whereClause.append(" AND (");
- for (String queryField : queryFields) {
-
- if (anyAdded) {
- whereClause.append(" OR ");
- }
- whereClause.append(queryField).append(" LIKE ? ");
- args.add(likeString);
- anyAdded = true;
- }
-
-
- whereClause.append(" )");
+ // TODO find beter solution
+ private static boolean isFolderId(String str) {
+ if (str == null) {
+ return false;
}
- if (folders != null && !folders.isEmpty()) {
- whereClause.append(" AND folder_id in (");
- boolean anyAdded = false;
- for (LocalFolder folder : folders) {
- if (anyAdded) {
- whereClause.append(",");
- }
- anyAdded = true;
- whereClause.append("?");
- args.add(Long.toString(folder.getId()));
- }
- whereClause.append(" )");
+ int length = str.length();
+ if (length == 0) {
+ return false;
}
- if (messages != null && messages.length > 0) {
- whereClause.append(" AND ( ");
- boolean anyAdded = false;
- for (Message message : messages) {
- if (anyAdded) {
- whereClause.append(" OR ");
- }
- anyAdded = true;
- whereClause.append(" ( uid = ? AND folder_id = ? ) ");
- args.add(message.getUid());
- args.add(Long.toString(((LocalFolder)message.getFolder()).getId()));
- }
- whereClause.append(" )");
+ int i = 0;
+ if (str.charAt(0) == '-') {
+ return false;
}
- if (forbiddenFlags != null && forbiddenFlags.length > 0) {
- whereClause.append(" AND (");
- boolean anyAdded = false;
- for (Flag flag : forbiddenFlags) {
- if (anyAdded) {
- whereClause.append(" AND ");
+ for (; i < length; i++) {
+ char c = str.charAt(i);
+ if (c <= '/' || c >= ':') {
+ return false;
}
- anyAdded = true;
- whereClause.append(" flags NOT LIKE ?");
+ }
+ return true;
+ }
- args.add("%" + flag.toString() + "%");
- }
- whereClause.append(" )");
- }
- if (requiredFlags != null && requiredFlags.length > 0) {
- whereClause.append(" AND (");
- boolean anyAdded = false;
- for (Flag flag : requiredFlags) {
- if (anyAdded) {
- whereClause.append(" OR ");
- }
- anyAdded = true;
- whereClause.append(" flags LIKE ?");
+ public Message[] searchForMessages(MessageRetrievalListener retrievalListener,
+ LocalSearch search) throws MessagingException {
- args.add("%" + flag.toString() + "%");
- }
- whereClause.append(" )");
- }
+ StringBuilder query = new StringBuilder();
+ List queryArgs = new ArrayList();
+ SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs);
+
+ // Avoid "ambiguous column name" error by prefixing "id" with the message table name
+ String where = SqlQueryBuilder.addPrefixToSelection(new String[] { "id" },
+ "messages.", query.toString());
+
+ String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
+
+ String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
+ "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " +
+ "((empty IS NULL OR empty != 1) AND deleted = 0)" +
+ ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
+ " ORDER BY date DESC";
if (K9.DEBUG) {
- Log.v(K9.LOG_TAG, "whereClause = " + whereClause.toString());
- Log.v(K9.LOG_TAG, "args = " + args);
+ Log.d(K9.LOG_TAG, "Query = " + sqlQuery);
}
- return getMessages(
- listener,
- null,
- "SELECT "
- + GET_MESSAGES_COLS
- + "FROM messages WHERE deleted = 0 " + whereClause.toString() + " ORDER BY date DESC"
- , args.toArray(EMPTY_STRING_ARRAY)
- );
+
+ return getMessages(retrievalListener, null, sqlQuery, selectionArgs);
}
+
/*
* Given a query string, actually do the query for the messages and
* call the MessageRetrievalListener for each one
@@ -1003,6 +1051,16 @@ public class LocalStore extends Store implements Serializable {
}
+ public Message[] getMessagesInThread(final long rootId) throws MessagingException {
+ String rootIdString = Long.toString(rootId);
+
+ LocalSearch search = new LocalSearch();
+ search.and(Searchfield.THREAD_ROOT, rootIdString, Attribute.EQUALS);
+ search.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, rootIdString));
+
+ return searchForMessages(null, search);
+ }
+
public AttachmentInfo getAttachmentInfo(final String attachmentId) throws UnavailableStorageException {
return database.execute(false, new DbCallback() {
@Override
@@ -1285,7 +1343,7 @@ public class LocalStore extends Store implements Serializable {
}
Cursor cursor = null;
try {
- cursor = db.rawQuery("SELECT COUNT(*) FROM messages WHERE deleted = 0 and folder_id = ?",
+ cursor = db.rawQuery("SELECT COUNT(*) FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 and folder_id = ?",
new String[] {
Long.toString(mFolderId)
});
@@ -1901,7 +1959,7 @@ public class LocalStore extends Store implements Serializable {
listener,
LocalFolder.this,
"SELECT " + GET_MESSAGES_COLS
- + "FROM messages WHERE "
+ + "FROM messages WHERE (empty IS NULL OR empty != 1) AND "
+ (includeDeleted ? "" : "deleted = 0 AND ")
+ " folder_id = ? ORDER BY date DESC"
, new String[] {
@@ -1918,7 +1976,6 @@ public class LocalStore extends Store implements Serializable {
}
}
-
@Override
public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
throws MessagingException {
@@ -1975,30 +2032,118 @@ public class LocalStore extends Store implements Serializable {
String oldUID = message.getUid();
- if (K9.DEBUG)
+ if (K9.DEBUG) {
Log.d(K9.LOG_TAG, "Updating folder_id to " + lDestFolder.getId() + " for message with UID "
+ message.getUid() + ", id " + lMessage.getId() + " currently in folder " + getName());
+ }
String newUid = K9.LOCAL_UID_PREFIX + UUID.randomUUID().toString();
message.setUid(newUid);
uidMap.put(oldUID, newUid);
- db.execSQL("UPDATE messages " + "SET folder_id = ?, uid = ? " + "WHERE id = ?", new Object[] {
- lDestFolder.getId(),
- message.getUid(),
- lMessage.getId()
- });
+ // Message threading in the target folder
+ ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message);
+
+ /*
+ * "Move" the message into the new folder and thread structure
+ */
+ long id = lMessage.getId();
+ String[] idArg = new String[] { Long.toString(id) };
+
+ ContentValues cv = new ContentValues();
+ cv.put("folder_id", lDestFolder.getId());
+ cv.put("uid", newUid);
+
+ if (threadInfo.rootId != -1) {
+ cv.put("thread_root", threadInfo.rootId);
+ }
+
+ if (threadInfo.parentId != -1) {
+ cv.put("thread_parent", threadInfo.parentId);
+ }
+
+ db.update("messages", cv, "id = ?", idArg);
+
+ if (threadInfo.id != -1) {
+ String[] oldIdArg =
+ new String[] { Long.toString(threadInfo.id) };
+
+ cv.clear();
+ cv.put("thread_root", id);
+ db.update("messages", cv, "thread_root = ?", oldIdArg);
+
+ cv.clear();
+ cv.put("thread_parent", id);
+ db.update("messages", cv, "thread_parent = ?", oldIdArg);
+ }
/*
* Add a placeholder message so we won't download the original
* message again if we synchronize before the remote move is
* complete.
*/
- LocalMessage placeHolder = new LocalMessage(oldUID, LocalFolder.this);
- placeHolder.setFlagInternal(Flag.DELETED, true);
- placeHolder.setFlagInternal(Flag.SEEN, true);
- appendMessages(new Message[] { placeHolder });
+
+ // We need to open this folder to get the folder id
+ open(OpenMode.READ_WRITE);
+
+ cv.clear();
+ cv.put("uid", oldUID);
+ cv.put("flags", serializeFlags(new Flag[] { Flag.DELETED, Flag.SEEN }));
+ cv.put("deleted", 1);
+ cv.put("folder_id", mFolderId);
+
+ String messageId = message.getMessageId();
+ if (messageId != null) {
+ cv.put("message_id", messageId);
+ cv.put("empty", 1);
+
+ long rootId = lMessage.getRootId();
+ if (rootId != -1) {
+ cv.put("thread_root", rootId);
+ }
+
+ long parentId = lMessage.getParentId();
+ if (parentId != -1) {
+ cv.put("thread_parent", parentId);
+ }
+ }
+
+ final long newId;
+ if (threadInfo.id != -1) {
+ // There already existed an empty message in the target folder.
+ // Let's use it as placeholder.
+
+ newId = threadInfo.id;
+
+ db.update("messages", cv, "id = ?",
+ new String[] { Long.toString(newId) });
+ } else {
+ newId = db.insert("messages", null, cv);
+ }
+
+ /*
+ * Replace all "thread links" to the original message with links to
+ * the placeholder message.
+ */
+
+ String[] whereArgs = new String[] {
+ Long.toString(mFolderId),
+ Long.toString(id) };
+
+ // Note: If there was an empty message in the target folder we
+ // already reconnected some messages to point to 'id'. We don't
+ // want to change those links again, so we limit the update
+ // statements below to the source folder.
+ cv.clear();
+ cv.put("thread_root", newId);
+ db.update("messages", cv, "folder_id = ? AND thread_root = ?",
+ whereArgs);
+
+ cv.clear();
+ cv.put("thread_parent", newId);
+ db.update("messages", cv, "folder_id = ? AND thread_parent = ?",
+ whereArgs);
}
} catch (MessagingException e) {
throw new WrappedException(e);
@@ -2006,6 +2151,9 @@ public class LocalStore extends Store implements Serializable {
return null;
}
});
+
+ notifyChange();
+
return uidMap;
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
@@ -2076,6 +2224,70 @@ public class LocalStore extends Store implements Serializable {
}
}
+ /**
+ * Get the (database) ID of the message with the specified message ID.
+ *
+ * @param db
+ * A {@link SQLiteDatabase} instance to access the database.
+ * @param messageId
+ * The message ID to search for.
+ * @param onlyEmptyMessages
+ * {@code true} if only "empty messages" (placeholders for threading) should be
+ * searched
+ *
+ * @return If exactly one message with the specified message ID was found, the database ID
+ * of this message; {@code -1} otherwise.
+ */
+ private long getDatabaseIdByMessageId(SQLiteDatabase db, String messageId,
+ boolean onlyEmptyMessages) {
+ long id = -1;
+
+ Cursor cursor = db.query("messages",
+ new String[] { "id" },
+ (onlyEmptyMessages) ?
+ "empty=1 AND folder_id=? AND message_id=?" :
+ "folder_id=? AND message_id=?",
+ new String[] { Long.toString(mFolderId), messageId },
+ null, null, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ id = cursor.getLong(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return id;
+ }
+
+ private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId) {
+ Cursor cursor = db.query("messages",
+ new String[] { "id", "thread_root", "thread_parent" },
+ "folder_id=? AND message_id=?",
+ new String[] { Long.toString(mFolderId), messageId },
+ null, null, null);
+
+ if (cursor != null) {
+ try {
+ if (cursor.getCount() == 1) {
+ cursor.moveToFirst();
+ long id = cursor.getLong(0);
+ long rootId = (cursor.isNull(1)) ? -1 : cursor.getLong(1);
+ long parentId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
+
+ return new ThreadInfo(id, messageId, rootId, parentId);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ return null;
+ }
/**
* The method differs slightly from the contract; If an incoming message already has a uid
@@ -2143,6 +2355,17 @@ public class LocalStore extends Store implements Serializable {
deleteAttachments(message.getUid());
}
+ long rootId = -1;
+ long parentId = -1;
+
+ if (oldMessageId == -1) {
+ // This is a new message. Do the message threading.
+ ThreadInfo threadInfo = doMessageThreading(db, message);
+ oldMessageId = threadInfo.id;
+ rootId = threadInfo.rootId;
+ parentId = threadInfo.parentId;
+ }
+
boolean isDraft = (message.getHeader(K9.IDENTITY_HEADER) != null);
List attachments;
@@ -2176,7 +2399,7 @@ public class LocalStore extends Store implements Serializable {
cv.put("sender_list", Address.pack(message.getFrom()));
cv.put("date", message.getSentDate() == null
? System.currentTimeMillis() : message.getSentDate().getTime());
- cv.put("flags", Utility.combine(message.getFlags(), ',').toUpperCase(Locale.US));
+ cv.put("flags", serializeFlags(message.getFlags()));
cv.put("deleted", message.isSet(Flag.DELETED) ? 1 : 0);
cv.put("folder_id", mFolderId);
cv.put("to_list", Address.pack(message.getRecipients(RecipientType.TO)));
@@ -2190,11 +2413,20 @@ public class LocalStore extends Store implements Serializable {
cv.put("internal_date", message.getInternalDate() == null
? System.currentTimeMillis() : message.getInternalDate().getTime());
cv.put("mime_type", message.getMimeType());
+ cv.put("empty", 0);
String messageId = message.getMessageId();
if (messageId != null) {
cv.put("message_id", messageId);
}
+
+ if (rootId != -1) {
+ cv.put("thread_root", rootId);
+ }
+ if (parentId != -1) {
+ cv.put("thread_parent", parentId);
+ }
+
long messageUid;
if (oldMessageId == -1) {
@@ -2223,6 +2455,9 @@ public class LocalStore extends Store implements Serializable {
return null;
}
});
+
+ notifyChange();
+
return uidMap;
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
@@ -2270,7 +2505,7 @@ public class LocalStore extends Store implements Serializable {
message.getSentDate() == null ? System
.currentTimeMillis() : message.getSentDate()
.getTime(),
- Utility.combine(message.getFlags(), ',').toUpperCase(Locale.US),
+ serializeFlags(message.getFlags()),
mFolderId,
Address.pack(message
.getRecipients(RecipientType.TO)),
@@ -2303,6 +2538,8 @@ public class LocalStore extends Store implements Serializable {
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
+
+ notifyChange();
}
/**
@@ -2337,7 +2574,8 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("UPDATE messages " + "SET flags = ? " + " WHERE id = ?",
new Object[]
- { Utility.combine(appendedFlags.toArray(), ',').toUpperCase(Locale.US), id });
+ { serializeFlags(appendedFlags.toArray(EMPTY_FLAG_ARRAY)), id });
+
return null;
}
});
@@ -2551,6 +2789,9 @@ public class LocalStore extends Store implements Serializable {
return null;
}
});
+
+ //TODO: remove this once the UI code exclusively uses the database id
+ notifyChange();
}
@Override
@@ -2581,7 +2822,7 @@ public class LocalStore extends Store implements Serializable {
Message[] messages = LocalStore.this.getMessages(
null,
this,
- "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE " + whereClause,
+ "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE (empty IS NULL OR empty != 1) AND (" + whereClause + ")",
params);
for (Message message : messages) {
@@ -2595,6 +2836,8 @@ public class LocalStore extends Store implements Serializable {
}
});
resetUnreadAndFlaggedCounts();
+
+ notifyChange();
}
public void clearMessagesOlderThan(long cutoff) throws MessagingException {
@@ -2883,6 +3126,167 @@ public class LocalStore extends Store implements Serializable {
});
}
+ private String serializeFlags(Flag[] flags) {
+ return Utility.combine(flags, ',').toUpperCase(Locale.US);
+ }
+
+ private ThreadInfo doMessageThreading(SQLiteDatabase db, Message message)
+ throws MessagingException {
+ long rootId = -1;
+ long parentId = -1;
+
+ String messageId = message.getMessageId();
+
+ // If there's already an empty message in the database, update that
+ long id = getDatabaseIdByMessageId(db, messageId, true);
+
+ // Get the message IDs from the "References" header line
+ String[] referencesArray = message.getHeader("References");
+ List messageIds = null;
+ if (referencesArray != null && referencesArray.length > 0) {
+ messageIds = Utility.extractMessageIds(referencesArray[0]);
+ }
+
+ // Append the first message ID from the "In-Reply-To" header line
+ String[] inReplyToArray = message.getHeader("In-Reply-To");
+ String inReplyTo = null;
+ if (inReplyToArray != null && inReplyToArray.length > 0) {
+ inReplyTo = Utility.extractMessageId(inReplyToArray[0]);
+ if (inReplyTo != null) {
+ if (messageIds == null) {
+ messageIds = new ArrayList(1);
+ messageIds.add(inReplyTo);
+ } else if (!messageIds.contains(inReplyTo)) {
+ messageIds.add(inReplyTo);
+ }
+ }
+ }
+
+ if (messageIds == null) {
+ // This is not a reply, nothing to do for us.
+ return new ThreadInfo(id, messageId, -1, -1);
+ }
+
+ for (String reference : messageIds) {
+ ThreadInfo threadInfo = getThreadInfo(db, reference);
+
+ if (threadInfo == null) {
+ // Create placeholder message
+ ContentValues cv = new ContentValues();
+ cv.put("message_id", reference);
+ cv.put("folder_id", mFolderId);
+ cv.put("empty", 1);
+
+ if (rootId != -1) {
+ cv.put("thread_root", rootId);
+ }
+ if (parentId != -1) {
+ cv.put("thread_parent", parentId);
+ }
+
+ parentId = db.insert("messages", null, cv);
+ if (rootId == -1) {
+ rootId = parentId;
+ }
+ } else {
+ if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.id) {
+ // We found an existing root container that is not
+ // the root of our current path (References).
+ // Connect it to the current parent.
+
+ // Let all children know who's the new root
+ ContentValues cv = new ContentValues();
+ cv.put("thread_root", rootId);
+ db.update("messages", cv, "thread_root=?",
+ new String[] { Long.toString(threadInfo.id) });
+
+ // Connect the message to the current parent
+ cv.put("thread_parent", parentId);
+ db.update("messages", cv, "id=?",
+ new String[] { Long.toString(threadInfo.id) });
+ } else {
+ rootId = (threadInfo.rootId == -1) ? threadInfo.id : threadInfo.rootId;
+ }
+ parentId = threadInfo.id;
+ }
+ }
+
+ //TODO: set in-reply-to "link" even if one already exists
+
+ return new ThreadInfo(id, messageId, rootId, parentId);
+ }
+
+ public List extractNewMessages(final List messages)
+ throws MessagingException {
+
+ try {
+ return database.execute(false, new DbCallback>() {
+ @Override
+ public List doDbWork(final SQLiteDatabase db) throws WrappedException {
+ try {
+ open(OpenMode.READ_WRITE);
+ } catch (MessagingException e) {
+ throw new WrappedException(e);
+ }
+
+ List result = new ArrayList();
+
+ List selectionArgs = new ArrayList();
+ Set existingMessages = new HashSet();
+ int start = 0;
+
+ while (start < messages.size()) {
+ StringBuilder selection = new StringBuilder();
+
+ selection.append("folder_id = ? AND UID IN (");
+ selectionArgs.add(Long.toString(mFolderId));
+
+ int count = Math.min(messages.size() - start, UID_CHECK_BATCH_SIZE);
+
+ for (int i = start, end = start + count; i < end; i++) {
+ if (i > start) {
+ selection.append(",?");
+ } else {
+ selection.append("?");
+ }
+
+ selectionArgs.add(messages.get(i).getUid());
+ }
+
+ selection.append(")");
+
+ Cursor cursor = db.query("messages", UID_CHECK_PROJECTION,
+ selection.toString(), selectionArgs.toArray(EMPTY_STRING_ARRAY),
+ null, null, null);
+
+ try {
+ while (cursor.moveToNext()) {
+ String uid = cursor.getString(0);
+ existingMessages.add(uid);
+ }
+ } finally {
+ Utility.closeQuietly(cursor);
+ }
+
+ for (int i = start, end = start + count; i < end; i++) {
+ Message message = messages.get(i);
+ if (!existingMessages.contains(message.getUid())) {
+ result.add(message);
+ }
+ }
+
+ existingMessages.clear();
+ selectionArgs.clear();
+ start += count;
+ }
+
+ return result;
+ }
+ });
+ } catch (WrappedException e) {
+ throw(MessagingException) e.getCause();
+ }
+ }
}
public static class LocalTextBody extends TextBody {
@@ -2917,19 +3321,12 @@ public class LocalStore extends Store implements Serializable {
private String mPreview = "";
- private boolean mToMeCalculated = false;
- private boolean mCcMeCalculated = false;
- private boolean mFromMeCalculated = false;
- private boolean mToMe = false;
- private boolean mCcMe = false;
- private boolean mFromMe = false;
-
-
-
-
private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false;
+ private long mRootId;
+ private long mParentId;
+
public LocalMessage() {
}
@@ -2983,6 +3380,9 @@ public class LocalStore extends Store implements Serializable {
f.open(LocalFolder.OpenMode.READ_WRITE);
this.mFolder = f;
}
+
+ mRootId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
+ mParentId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
}
/**
@@ -3122,64 +3522,6 @@ public class LocalStore extends Store implements Serializable {
mMessageDirty = true;
}
-
- public boolean fromMe() {
- if (!mFromMeCalculated) {
- if (mAccount.isAnIdentity(getFrom())) {
- mFromMe = true;
- mFromMeCalculated = true;
- }
- }
- return mFromMe;
- }
-
-
- public boolean toMe() {
- try {
- if (!mToMeCalculated) {
- for (Address address : getRecipients(RecipientType.TO)) {
- if (mAccount.isAnIdentity(address)) {
- mToMe = true;
- mToMeCalculated = true;
- }
- }
- }
- } catch (MessagingException e) {
- // do something better than ignore this
- // getRecipients can throw a messagingexception
- }
- return mToMe;
- }
-
-
-
-
-
- public boolean ccMe() {
- try {
-
- if (!mCcMeCalculated) {
- for (Address address : getRecipients(RecipientType.CC)) {
- if (mAccount.isAnIdentity(address)) {
- mCcMe = true;
- mCcMeCalculated = true;
- }
- }
-
- }
- } catch (MessagingException e) {
- // do something better than ignore this
- // getRecipients can throw a messagingexception
- }
-
- return mCcMe;
- }
-
-
-
-
-
-
public void setFlagInternal(Flag flag, boolean set) throws MessagingException {
super.setFlag(flag, set);
}
@@ -3219,7 +3561,7 @@ public class LocalStore extends Store implements Serializable {
throw(MessagingException) e.getCause();
}
-
+ notifyChange();
}
/*
@@ -3236,13 +3578,26 @@ public class LocalStore extends Store implements Serializable {
try {
database.execute(true, new DbCallback() {
@Override
- public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
- db.execSQL("UPDATE messages SET " + "deleted = 1," + "subject = NULL, "
- + "sender_list = NULL, " + "date = NULL, " + "to_list = NULL, "
- + "cc_list = NULL, " + "bcc_list = NULL, " + "preview = NULL, "
- + "html_content = NULL, " + "text_content = NULL, "
- + "reply_to_list = NULL " + "WHERE id = ?", new Object[]
- { mId });
+ public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+ String[] idArg = new String[] { Long.toString(mId) };
+
+ ContentValues cv = new ContentValues();
+ cv.put("deleted", 1);
+ cv.put("empty", 1);
+ cv.putNull("subject");
+ cv.putNull("sender_list");
+ cv.putNull("date");
+ cv.putNull("to_list");
+ cv.putNull("cc_list");
+ cv.putNull("bcc_list");
+ cv.putNull("preview");
+ cv.putNull("html_content");
+ cv.putNull("text_content");
+ cv.putNull("reply_to_list");
+
+ db.update("messages", cv, "id = ?", idArg);
+
/*
* Delete all of the message's attachments to save space.
* We do this explicit deletion here because we're not deleting the record
@@ -3253,8 +3608,8 @@ public class LocalStore extends Store implements Serializable {
} catch (MessagingException e) {
throw new WrappedException(e);
}
- db.execSQL("DELETE FROM attachments WHERE message_id = ?", new Object[]
- { mId });
+
+ db.delete("attachments", "message_id = ?", idArg);
return null;
}
});
@@ -3263,11 +3618,13 @@ public class LocalStore extends Store implements Serializable {
}
((LocalFolder)mFolder).deleteHeaders(mId);
-
+ notifyChange();
}
/*
* Completely remove a message from the local database
+ *
+ * TODO: document how this updates the thread structure
*/
@Override
public void destroy() throws MessagingException {
@@ -3277,9 +3634,102 @@ public class LocalStore extends Store implements Serializable {
public Void doDbWork(final SQLiteDatabase db) throws WrappedException,
UnavailableStorageException {
try {
+ LocalFolder localFolder = (LocalFolder) mFolder;
+
updateFolderCountsOnFlag(Flag.X_DESTROYED, true);
- ((LocalFolder) mFolder).deleteAttachments(mId);
- db.execSQL("DELETE FROM messages WHERE id = ?", new Object[] { mId });
+ localFolder.deleteAttachments(mId);
+
+ String id = Long.toString(mId);
+
+ // Check if this message has children in the thread hierarchy
+ Cursor cursor = db.query("messages", new String[] { "COUNT(id)" },
+ "thread_root = ? OR thread_parent = ?",
+ new String[] {id, id},
+ null, null, null);
+
+ try {
+ if (cursor.moveToFirst() && cursor.getLong(0) > 0) {
+ // Make the message an empty message
+ ContentValues cv = new ContentValues();
+ cv.put("id", mId);
+ cv.put("folder_id", localFolder.getId());
+ cv.put("deleted", 0);
+ cv.put("message_id", getMessageId());
+ cv.put("empty", 1);
+
+ if (getRootId() != -1) {
+ cv.put("thread_root", getRootId());
+ }
+
+ if (getParentId() != -1) {
+ cv.put("thread_parent", getParentId());
+ }
+
+ db.replace("messages", null, cv);
+
+ // Nothing else to do
+ return null;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ long parentId = getParentId();
+
+ // Check if 'parentId' is empty
+ cursor = db.query("messages", new String[] { "id" },
+ "id = ? AND empty = 1",
+ new String[] { Long.toString(parentId) },
+ null, null, null);
+
+ try {
+ if (cursor.getCount() == 0) {
+ // If the message isn't empty we skip the loop below
+ parentId = -1;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ while (parentId != -1) {
+ String parentIdString = Long.toString(parentId);
+
+ // Get the parent of the message 'parentId'
+ cursor = db.query("messages", new String[] { "thread_parent" },
+ "id = ? AND empty = 1",
+ new String[] { parentIdString },
+ null, null, null);
+ try {
+ if (cursor.moveToFirst() && !cursor.isNull(0)) {
+ parentId = cursor.getLong(0);
+ } else {
+ parentId = -1;
+ }
+ } finally {
+ cursor.close();
+ }
+
+ // Check if (the old) 'parentId' has any children
+ cursor = db.query("messages", new String[] { "COUNT(id)" },
+ "thread_parent = ? AND id != ?",
+ new String[] { parentIdString, id },
+ null, null, null);
+
+ try {
+ if (cursor.moveToFirst() && cursor.getLong(0) == 0) {
+ // If it has no children we can remove it
+ db.delete("messages", "id = ?",
+ new String[] { parentIdString });
+ } else {
+ break;
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+
+ // Remove the placeholder message
+ db.delete("messages", "id = ?", new String[] { id });
} catch (MessagingException e) {
throw new WrappedException(e);
}
@@ -3289,6 +3739,8 @@ public class LocalStore extends Store implements Serializable {
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
+
+ notifyChange();
}
private void updateFolderCountsOnFlag(Flag flag, boolean set) {
@@ -3376,15 +3828,19 @@ public class LocalStore extends Store implements Serializable {
message.mAttachmentCount = mAttachmentCount;
message.mSubject = mSubject;
message.mPreview = mPreview;
- message.mToMeCalculated = mToMeCalculated;
- message.mCcMeCalculated = mCcMeCalculated;
- message.mToMe = mToMe;
- message.mCcMe = mCcMe;
message.mHeadersLoaded = mHeadersLoaded;
message.mMessageDirty = mMessageDirty;
return message;
}
+
+ public long getRootId() {
+ return mRootId;
+ }
+
+ public long getParentId() {
+ return mParentId;
+ }
}
public static class LocalAttachmentBodyPart extends MimeBodyPart {
@@ -3449,4 +3905,27 @@ public class LocalStore extends Store implements Serializable {
return mUri;
}
}
+
+ static class ThreadInfo {
+ public final long id;
+ public final String messageId;
+ public final long rootId;
+ public final long parentId;
+
+ public ThreadInfo(long id, String messageId, long rootId, long parentId) {
+ this.id = id;
+ this.messageId = messageId;
+ this.rootId = rootId;
+ this.parentId = parentId;
+ }
+ }
+
+ public LockableDatabase getDatabase() {
+ return database;
+ }
+
+ private void notifyChange() {
+ Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages");
+ mContentResolver.notifyChange(uri, null);
+ }
}
diff --git a/src/com/fsck/k9/mail/store/LockableDatabase.java b/src/com/fsck/k9/mail/store/LockableDatabase.java
index b303c2aef..67090aace 100644
--- a/src/com/fsck/k9/mail/store/LockableDatabase.java
+++ b/src/com/fsck/k9/mail/store/LockableDatabase.java
@@ -49,7 +49,7 @@ public class LockableDatabase {
* Workaround exception wrapper used to keep the inner exception generated
* in a {@link DbCallback}.
*/
- protected static class WrappedException extends RuntimeException {
+ public static class WrappedException extends RuntimeException {
/**
*
*/
diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java
index 985826605..190b46765 100644
--- a/src/com/fsck/k9/preferences/GlobalSettings.java
+++ b/src/com/fsck/k9/preferences/GlobalSettings.java
@@ -222,6 +222,9 @@ public class GlobalSettings {
s.put("useBackgroundAsUnreadIndicator", Settings.versions(
new V(19, new BooleanSetting(true))
));
+ s.put("threadedView", Settings.versions(
+ new V(20, new BooleanSetting(true))
+ ));
SETTINGS = Collections.unmodifiableMap(s);
diff --git a/src/com/fsck/k9/preferences/Settings.java b/src/com/fsck/k9/preferences/Settings.java
index 725c562f7..56a6646b9 100644
--- a/src/com/fsck/k9/preferences/Settings.java
+++ b/src/com/fsck/k9/preferences/Settings.java
@@ -35,7 +35,7 @@ public class Settings {
*
* @see SettingsExporter
*/
- public static final int VERSION = 19;
+ public static final int VERSION = 20;
public static Map validate(int version, Map> settings,
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
new file mode 100644
index 000000000..150c272dc
--- /dev/null
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -0,0 +1,579 @@
+package com.fsck.k9.provider;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.Preferences;
+import com.fsck.k9.helper.StringUtils;
+import com.fsck.k9.helper.Utility;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.store.LocalStore;
+import com.fsck.k9.mail.store.LockableDatabase;
+import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
+import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
+import com.fsck.k9.mail.store.UnavailableStorageException;
+import com.fsck.k9.search.SqlQueryBuilder;
+
+import android.annotation.TargetApi;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.CursorWrapper;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.Uri;
+
+/**
+ * Content Provider used to display the message list etc.
+ *
+ *
+ * For now this content provider is for internal use only. In the future we may allow third-party
+ * apps to access K-9 Mail content using this content provider.
+ *
+ */
+/*
+ * TODO:
+ * - add support for account list and folder list
+ */
+public class EmailProvider extends ContentProvider {
+ private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
+
+ public static final String AUTHORITY = "org.k9mail.provider.email";
+
+ public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
+
+
+ /*
+ * Constants that are used for the URI matching.
+ */
+ private static final int MESSAGE_BASE = 0;
+ private static final int MESSAGES = MESSAGE_BASE;
+ private static final int MESSAGES_THREADED = MESSAGE_BASE + 1;
+ //private static final int MESSAGES_THREAD = MESSAGE_BASE + 2;
+
+
+ private static final String MESSAGES_TABLE = "messages";
+
+ private static final String[] MESSAGES_COLUMNS = {
+ MessageColumns.ID,
+ MessageColumns.UID,
+ MessageColumns.INTERNAL_DATE,
+ MessageColumns.SUBJECT,
+ MessageColumns.DATE,
+ MessageColumns.MESSAGE_ID,
+ MessageColumns.SENDER_LIST,
+ MessageColumns.TO_LIST,
+ MessageColumns.CC_LIST,
+ MessageColumns.BCC_LIST,
+ MessageColumns.REPLY_TO_LIST,
+ MessageColumns.FLAGS,
+ MessageColumns.ATTACHMENT_COUNT,
+ MessageColumns.FOLDER_ID,
+ MessageColumns.PREVIEW,
+ MessageColumns.THREAD_ROOT,
+ MessageColumns.THREAD_PARENT,
+ InternalMessageColumns.DELETED,
+ InternalMessageColumns.EMPTY,
+ InternalMessageColumns.TEXT_CONTENT,
+ InternalMessageColumns.HTML_CONTENT,
+ InternalMessageColumns.MIME_TYPE
+ };
+
+ static {
+ UriMatcher matcher = sUriMatcher;
+
+ matcher.addURI(AUTHORITY, "account/*/messages", MESSAGES);
+ matcher.addURI(AUTHORITY, "account/*/messages/threaded", MESSAGES_THREADED);
+ //matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
+ }
+
+ public interface SpecialColumns {
+ public static final String ACCOUNT_UUID = "account_uuid";
+
+ public static final String FOLDER_NAME = "name";
+ public static final String INTEGRATE = "integrate";
+ }
+
+ public interface MessageColumns {
+ public static final String ID = "id";
+ public static final String UID = "uid";
+ public static final String INTERNAL_DATE = "internal_date";
+ public static final String SUBJECT = "subject";
+ public static final String DATE = "date";
+ public static final String MESSAGE_ID = "message_id";
+ public static final String SENDER_LIST = "sender_list";
+ public static final String TO_LIST = "to_list";
+ public static final String CC_LIST = "cc_list";
+ public static final String BCC_LIST = "bcc_list";
+ public static final String REPLY_TO_LIST = "reply_to_list";
+ public static final String FLAGS = "flags";
+ public static final String ATTACHMENT_COUNT = "attachment_count";
+ public static final String FOLDER_ID = "folder_id";
+ public static final String PREVIEW = "preview";
+ public static final String THREAD_ROOT = "thread_root";
+ public static final String THREAD_PARENT = "thread_parent";
+ public static final String THREAD_COUNT = "thread_count";
+ }
+
+ private interface InternalMessageColumns extends MessageColumns {
+ public static final String DELETED = "deleted";
+ public static final String EMPTY = "empty";
+ public static final String TEXT_CONTENT = "text_content";
+ public static final String HTML_CONTENT = "html_content";
+ public static final String MIME_TYPE = "mime_type";
+ }
+
+
+ private Preferences mPreferences;
+
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ throw new RuntimeException("not implemented yet");
+ }
+
+ @Override
+ public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
+ String sortOrder) {
+
+ int match = sUriMatcher.match(uri);
+ if (match < 0) {
+ throw new IllegalArgumentException("Unknown URI: " + uri);
+ }
+
+ ContentResolver contentResolver = getContext().getContentResolver();
+ Cursor cursor = null;
+ switch (match) {
+ case MESSAGES:
+ case MESSAGES_THREADED: {
+ List segments = uri.getPathSegments();
+ String accountUuid = segments.get(1);
+
+ List dbColumnNames = new ArrayList(projection.length);
+ Map specialColumns = new HashMap();
+ for (String columnName : projection) {
+ if (SpecialColumns.ACCOUNT_UUID.equals(columnName)) {
+ specialColumns.put(SpecialColumns.ACCOUNT_UUID, accountUuid);
+ } else {
+ dbColumnNames.add(columnName);
+ }
+ }
+
+ String[] dbProjection = dbColumnNames.toArray(new String[0]);
+
+ if (match == MESSAGES) {
+ cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs,
+ sortOrder);
+ } else if (match == MESSAGES_THREADED) {
+ cursor = getThreadedMessages(accountUuid, dbProjection, selection,
+ selectionArgs, sortOrder);
+ } else {
+ throw new RuntimeException("Not implemented");
+ }
+
+ cursor.setNotificationUri(contentResolver, uri);
+
+ cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection,
+ specialColumns);
+
+ break;
+ }
+ }
+
+ return cursor;
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new RuntimeException("not implemented yet");
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new RuntimeException("not implemented yet");
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new RuntimeException("not implemented yet");
+ }
+
+ protected Cursor getMessages(String accountUuid, final String[] projection,
+ final String selection, final String[] selectionArgs, final String sortOrder) {
+
+ Account account = getAccount(accountUuid);
+ LockableDatabase database = getDatabase(account);
+
+ try {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+
+ String where;
+ if (StringUtils.isNullOrEmpty(selection)) {
+ where = InternalMessageColumns.DELETED + "=0 AND (" +
+ InternalMessageColumns.EMPTY + " IS NULL OR " +
+ InternalMessageColumns.EMPTY + "!=1)";
+ } else {
+ where = "(" + selection + ") AND " +
+ InternalMessageColumns.DELETED + "=0 AND (" +
+ InternalMessageColumns.EMPTY + " IS NULL OR " +
+ InternalMessageColumns.EMPTY + "!=1)";
+ }
+
+ final Cursor cursor;
+ //TODO: check projection and selection for folder columns
+ if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) {
+ StringBuilder query = new StringBuilder();
+ query.append("SELECT ");
+ boolean first = true;
+ for (String columnName : projection) {
+ if (!first) {
+ query.append(",");
+ } else {
+ first = false;
+ }
+
+ if (MessageColumns.ID.equals(columnName)) {
+ query.append("m.");
+ query.append(MessageColumns.ID);
+ query.append(" AS ");
+ query.append(MessageColumns.ID);
+ } else {
+ query.append(columnName);
+ }
+ }
+
+ query.append(" FROM messages m " +
+ "LEFT JOIN folders f ON (m.folder_id = f.id) " +
+ "WHERE ");
+ query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+ "m.", where));
+ query.append(" ORDER BY ");
+ query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+ "m.", sortOrder));
+
+ cursor = db.rawQuery(query.toString(), selectionArgs);
+ } else {
+ cursor = db.query(MESSAGES_TABLE, projection, where, selectionArgs, null,
+ null, sortOrder);
+ }
+
+ return cursor;
+ }
+ });
+ } catch (UnavailableStorageException e) {
+ throw new RuntimeException("Storage not available", e);
+ }
+ }
+
+ protected Cursor getThreadedMessages(String accountUuid, final String[] projection,
+ final String selection, final String[] selectionArgs, final String sortOrder) {
+
+ Account account = getAccount(accountUuid);
+ LockableDatabase database = getDatabase(account);
+
+ try {
+ return database.execute(false, new DbCallback() {
+ @Override
+ public Cursor doDbWork(SQLiteDatabase db) throws WrappedException,
+ UnavailableStorageException {
+
+ StringBuilder query = new StringBuilder();
+ query.append("SELECT ");
+ boolean first = true;
+ for (String columnName : projection) {
+ if (!first) {
+ query.append(",");
+ } else {
+ first = false;
+ }
+
+ if (MessageColumns.DATE.equals(columnName)) {
+ query.append("MAX(m.date) AS " + MessageColumns.DATE);
+ } else if (MessageColumns.THREAD_COUNT.equals(columnName)) {
+ query.append("COUNT(h.id) AS " + MessageColumns.THREAD_COUNT);
+ } else if (SpecialColumns.FOLDER_NAME.equals(columnName)) {
+ query.append("f." + SpecialColumns.FOLDER_NAME + " AS " +
+ SpecialColumns.FOLDER_NAME);
+ } else if (SpecialColumns.INTEGRATE.equals(columnName)) {
+ query.append("f." + SpecialColumns.INTEGRATE + " AS " +
+ SpecialColumns.INTEGRATE);
+ } else {
+ query.append("m.");
+ query.append(columnName);
+ query.append(" AS ");
+ query.append(columnName);
+ }
+ }
+
+ query.append(
+ " FROM messages h JOIN messages m " +
+ "ON (h.id = m.thread_root OR h.id = m.id) ");
+
+ //TODO: check projection and selection for folder columns
+ if (Utility.arrayContains(projection, SpecialColumns.FOLDER_NAME)) {
+ query.append("LEFT JOIN folders f ON (m.folder_id = f.id) ");
+ }
+
+ query.append(
+ "WHERE " +
+ "(h.deleted = 0 AND m.deleted = 0 AND " +
+ "(m.empty IS NULL OR m.empty != 1) AND " +
+ "h.thread_root IS NULL) ");
+
+ if (!StringUtils.isNullOrEmpty(selection)) {
+ query.append("AND (");
+ query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+ "h.", selection));
+ query.append(") ");
+ }
+
+ query.append("GROUP BY h.id");
+
+ if (!StringUtils.isNullOrEmpty(sortOrder)) {
+ query.append(" ORDER BY ");
+ query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+ "m.", sortOrder));
+ }
+
+ return db.rawQuery(query.toString(), selectionArgs);
+ }
+ });
+ } catch (UnavailableStorageException e) {
+ throw new RuntimeException("Storage not available", e);
+ }
+ }
+
+ private Account getAccount(String accountUuid) {
+ if (mPreferences == null) {
+ Context appContext = getContext().getApplicationContext();
+ mPreferences = Preferences.getPreferences(appContext);
+ }
+
+ Account account = mPreferences.getAccount(accountUuid);
+
+ if (account == null) {
+ throw new IllegalArgumentException("Unknown account: " + accountUuid);
+ }
+
+ return account;
+ }
+
+ private LockableDatabase getDatabase(Account account) {
+ LocalStore localStore;
+ try {
+ localStore = account.getLocalStore();
+ } catch (MessagingException e) {
+ throw new RuntimeException("Couldn't get LocalStore", e);
+ }
+
+ return localStore.getDatabase();
+ }
+
+ /**
+ * This class is needed to make {@link CursorAdapter} work with our database schema.
+ *
+ *
+ * {@code CursorAdapter} requires a column named {@code "_id"} containing a stable id. We use
+ * the column name {@code "id"} as primary key in all our tables. So this {@link CursorWrapper}
+ * maps all queries for {@code "_id"} to {@code "id"}.
+ *
+ * Please note that this only works for the returned {@code Cursor}. When querying the content
+ * provider you still need to use {@link MessageColumns#ID}.
+ *
+ */
+ static class IdTrickeryCursor extends CursorWrapper {
+ public IdTrickeryCursor(Cursor cursor) {
+ super(cursor);
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ if ("_id".equals(columnName)) {
+ return super.getColumnIndex("id");
+ }
+
+ return super.getColumnIndex(columnName);
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) {
+ if ("_id".equals(columnName)) {
+ return super.getColumnIndexOrThrow("id");
+ }
+
+ return super.getColumnIndexOrThrow(columnName);
+ }
+ }
+
+ static class SpecialColumnsCursor extends CursorWrapper {
+ private int[] mColumnMapping;
+ private String[] mSpecialColumnValues;
+ private String[] mColumnNames;
+
+ public SpecialColumnsCursor(Cursor cursor, String[] allColumnNames,
+ Map specialColumns) {
+ super(cursor);
+
+ mColumnNames = allColumnNames;
+ mColumnMapping = new int[allColumnNames.length];
+ mSpecialColumnValues = new String[specialColumns.size()];
+ for (int i = 0, columnIndex = 0, specialColumnCount = 0, len = allColumnNames.length;
+ i < len; i++) {
+
+ String columnName = allColumnNames[i];
+
+ if (specialColumns.containsKey(columnName)) {
+ // This is a special column name, so save the value in mSpecialColumnValues
+ mSpecialColumnValues[specialColumnCount] = specialColumns.get(columnName);
+
+ // Write the index into mSpecialColumnValues negated into mColumnMapping
+ mColumnMapping[i] = -(specialColumnCount + 1);
+ specialColumnCount++;
+ } else {
+ mColumnMapping[i] = columnIndex++;
+ }
+ }
+ }
+
+ @Override
+ public byte[] getBlob(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getBlob(realColumnIndex);
+ }
+
+ @Override
+ public int getColumnCount() {
+ return mColumnMapping.length;
+ }
+
+ @Override
+ public int getColumnIndex(String columnName) {
+ for (int i = 0, len = mColumnNames.length; i < len; i++) {
+ if (mColumnNames[i].equals(columnName)) {
+ return i;
+ }
+ }
+
+ return super.getColumnIndex(columnName);
+ }
+
+ @Override
+ public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException {
+ int index = getColumnIndex(columnName);
+
+ if (index == -1) {
+ throw new IllegalArgumentException("Unknown column name");
+ }
+
+ return index;
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return mColumnNames[columnIndex];
+ }
+
+ @Override
+ public String[] getColumnNames() {
+ return mColumnNames.clone();
+ }
+
+ @Override
+ public double getDouble(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getDouble(realColumnIndex);
+ }
+
+ @Override
+ public float getFloat(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getFloat(realColumnIndex);
+ }
+
+ @Override
+ public int getInt(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getInt(realColumnIndex);
+ }
+
+ @Override
+ public long getLong(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getLong(realColumnIndex);
+ }
+
+ @Override
+ public short getShort(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ throw new RuntimeException("Special column can only be retrieved as string.");
+ }
+
+ return super.getShort(realColumnIndex);
+ }
+
+ @Override
+ public String getString(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ return mSpecialColumnValues[-realColumnIndex - 1];
+ }
+
+ return super.getString(realColumnIndex);
+ }
+
+ @TargetApi(11)
+ @Override
+ public int getType(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ return FIELD_TYPE_STRING;
+ }
+
+ return super.getType(realColumnIndex);
+ }
+
+ @Override
+ public boolean isNull(int columnIndex) {
+ int realColumnIndex = mColumnMapping[columnIndex];
+ if (realColumnIndex < 0) {
+ return (mSpecialColumnValues[-realColumnIndex - 1] == null);
+ }
+
+ return super.isNull(realColumnIndex);
+ }
+ }
+}
diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 1e351d057..7035baa65 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -21,23 +21,22 @@ import com.fsck.k9.Account;
import com.fsck.k9.AccountStats;
import com.fsck.k9.K9;
import com.fsck.k9.Preferences;
-import com.fsck.k9.SearchAccount;
import com.fsck.k9.activity.FolderInfoHolder;
import com.fsck.k9.activity.MessageInfoHolder;
-import com.fsck.k9.activity.MessageList;
import com.fsck.k9.controller.MessagingController;
import com.fsck.k9.controller.MessagingListener;
-import com.fsck.k9.fragment.MessageListFragment;
import com.fsck.k9.helper.MessageHelper;
import com.fsck.k9.mail.Flag;
import com.fsck.k9.mail.Folder;
import com.fsck.k9.mail.Message;
import com.fsck.k9.mail.MessagingException;
import com.fsck.k9.mail.store.LocalStore;
+import com.fsck.k9.search.SearchAccount;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.concurrent.BlockingQueue;
@@ -302,14 +301,13 @@ public class MessageProvider extends ContentProvider {
final SearchAccount integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(getContext());
final MessagingController msgController = MessagingController.getInstance(K9.app);
- msgController.searchLocalMessages(integratedInboxAccount, null,
+ msgController.searchLocalMessages(integratedInboxAccount.getRelatedSearch(),
new MesssageInfoHolderRetrieverListener(queue));
final List holders = queue.take();
// TODO add sort order parameter
- Collections.sort(holders, new MessageListFragment.ReverseComparator(
- new MessageListFragment.DateComparator()));
+ Collections.sort(holders, new ReverseDateComparator());
final String[] projectionToUse;
if (projection == null) {
@@ -1025,7 +1023,8 @@ public class MessageProvider extends ContentProvider {
// launch command to delete the message
if ((myAccount != null) && (msg != null)) {
- MessagingController.getInstance(K9.app).deleteMessages(new Message[] { msg }, null);
+ MessagingController controller = MessagingController.getInstance(K9.app);
+ controller.deleteMessages(Collections.singletonList(msg), null);
}
// FIXME return the actual number of deleted messages
@@ -1123,4 +1122,16 @@ public class MessageProvider extends ContentProvider {
mUriMatcher.addURI(AUTHORITY, handler.getPath(), code);
}
+ public static class ReverseDateComparator implements Comparator {
+ @Override
+ public int compare(MessageInfoHolder object2, MessageInfoHolder object1) {
+ if (object1.compareDate == null) {
+ return (object2.compareDate == null ? 0 : 1);
+ } else if (object2.compareDate == null) {
+ return -1;
+ } else {
+ return object1.compareDate.compareTo(object2.compareDate);
+ }
+ }
+ }
}
diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
index 2af479d15..75acd1073 100644
--- a/src/com/fsck/k9/provider/UnreadWidgetProvider.java
+++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
@@ -8,6 +8,7 @@ import com.fsck.k9.R;
import com.fsck.k9.activity.UnreadWidgetConfiguration;
import com.fsck.k9.activity.FolderList;
import com.fsck.k9.activity.MessageList;
+import com.fsck.k9.search.LocalSearch;
import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
@@ -62,8 +63,11 @@ public class UnreadWidgetProvider extends AppWidgetProvider {
clickIntent = FolderList.actionHandleAccountIntent(context, account, null,
false);
} else {
- clickIntent = MessageList.actionHandleFolderIntent(context, account,
- account.getAutoExpandFolderName());
+ LocalSearch search = new LocalSearch(account.getAutoExpandFolderName());
+ search.addAllowedFolder(account.getAutoExpandFolderName());
+ search.addAccountUuid(account.getUuid());
+ clickIntent = MessageList.intentDisplaySearch(context, search, false, true,
+ true);
}
clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
}
diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java
new file mode 100644
index 000000000..1a1be85d7
--- /dev/null
+++ b/src/com/fsck/k9/search/ConditionsTreeNode.java
@@ -0,0 +1,431 @@
+package com.fsck.k9.search;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Stack;
+
+import android.database.Cursor;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+import com.fsck.k9.search.SearchSpecification.SearchCondition;
+
+/**
+ * This class stores search conditions. It's basically a boolean expression binary tree.
+ * The output will be SQL queries ( obtained by traversing inorder ).
+ *
+ * TODO removing conditions from the tree
+ * TODO implement NOT as a node again
+ */
+public class ConditionsTreeNode implements Parcelable {
+
+ public enum Operator {
+ AND, OR, CONDITION;
+ }
+
+ public ConditionsTreeNode mLeft;
+ public ConditionsTreeNode mRight;
+ public ConditionsTreeNode mParent;
+
+ /*
+ * If mValue isn't CONDITION then mCondition contains a real
+ * condition, otherwise it's null.
+ */
+ public Operator mValue;
+ public SearchCondition mCondition;
+
+ /*
+ * Used for storing and retrieving the tree to/from the database.
+ * The algorithm is called "modified preorder tree traversal".
+ */
+ public int mLeftMPTTMarker;
+ public int mRightMPTTMarker;
+
+
+ ///////////////////////////////////////////////////////////////
+ // Static Helpers to restore a tree from a database cursor
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Builds a condition tree starting from a database cursor. The cursor
+ * should point to rows representing the nodes of the tree.
+ *
+ * @param cursor Cursor pointing to the first of a bunch or rows. Each rows
+ * should contains 1 tree node.
+ * @return A condition tree.
+ */
+ public static ConditionsTreeNode buildTreeFromDB(Cursor cursor) {
+ Stack stack = new Stack();
+ ConditionsTreeNode tmp = null;
+
+ // root node
+ if (cursor.moveToFirst()) {
+ tmp = buildNodeFromRow(cursor);
+ stack.push(tmp);
+ }
+
+ // other nodes
+ while (cursor.moveToNext()) {
+ tmp = buildNodeFromRow(cursor);
+ if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker) {
+ stack.peek().mLeft = tmp;
+ stack.push(tmp);
+ } else {
+ while (stack.peek().mRightMPTTMarker < tmp.mRightMPTTMarker) {
+ stack.pop();
+ }
+ stack.peek().mRight = tmp;
+ }
+ }
+ return tmp;
+ }
+
+ /**
+ * Converts a single database row to a single condition node.
+ *
+ * @param cursor Cursor pointing to the row we want to convert.
+ * @return A single ConditionsTreeNode
+ */
+ private static ConditionsTreeNode buildNodeFromRow(Cursor cursor) {
+ ConditionsTreeNode result = null;
+ SearchCondition condition = null;
+
+ Operator tmpValue = ConditionsTreeNode.Operator.valueOf(cursor.getString(5));
+
+ if (tmpValue == Operator.CONDITION) {
+ condition = new SearchCondition(Searchfield.valueOf(cursor.getString(0)),
+ Attribute.valueOf(cursor.getString(2)), cursor.getString(1));
+ }
+
+ result = new ConditionsTreeNode(condition);
+ result.mValue = tmpValue;
+ result.mLeftMPTTMarker = cursor.getInt(3);
+ result.mRightMPTTMarker = cursor.getInt(4);
+
+ return result;
+ }
+
+
+ ///////////////////////////////////////////////////////////////
+ // Constructors
+ ///////////////////////////////////////////////////////////////
+ public ConditionsTreeNode(SearchCondition condition) {
+ mParent = null;
+ mCondition = condition;
+ mValue = Operator.CONDITION;
+ }
+
+ public ConditionsTreeNode(ConditionsTreeNode parent, Operator op) {
+ mParent = parent;
+ mValue = op;
+ mCondition = null;
+ }
+
+
+ /* package */ ConditionsTreeNode cloneTree() {
+ if (mParent != null) {
+ throw new IllegalStateException("Can't call cloneTree() for a non-root node");
+ }
+
+ ConditionsTreeNode copy = new ConditionsTreeNode(mCondition.clone());
+
+ copy.mLeftMPTTMarker = mLeftMPTTMarker;
+ copy.mRightMPTTMarker = mRightMPTTMarker;
+
+ copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
+ copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
+
+ return copy;
+ }
+
+ private ConditionsTreeNode cloneNode(ConditionsTreeNode parent) {
+ ConditionsTreeNode copy = new ConditionsTreeNode(parent, mValue);
+
+ copy.mCondition = mCondition.clone();
+ copy.mLeftMPTTMarker = mLeftMPTTMarker;
+ copy.mRightMPTTMarker = mRightMPTTMarker;
+
+ copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
+ copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
+
+ return copy;
+ }
+
+ ///////////////////////////////////////////////////////////////
+ // Public modifiers
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Adds the expression as the second argument of an AND
+ * clause to this node.
+ *
+ * @param expr Expression to 'AND' with.
+ * @return New top AND node.
+ * @throws Exception
+ */
+ public ConditionsTreeNode and(ConditionsTreeNode expr) throws Exception {
+ return add(expr, Operator.AND);
+ }
+
+ /**
+ * Convenience method.
+ * Adds the provided condition as the second argument of an AND
+ * clause to this node.
+ *
+ * @param condition Condition to 'AND' with.
+ * @return New top AND node, new root.
+ */
+ public ConditionsTreeNode and(SearchCondition condition) {
+ try {
+ ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+ return and(tmp);
+ } catch (Exception e) {
+ // impossible
+ return null;
+ }
+ }
+
+ /**
+ * Adds the expression as the second argument of an OR
+ * clause to this node.
+ *
+ * @param expr Expression to 'OR' with.
+ * @return New top OR node.
+ * @throws Exception
+ */
+ public ConditionsTreeNode or(ConditionsTreeNode expr) throws Exception {
+ return add(expr, Operator.OR);
+ }
+
+ /**
+ * Convenience method.
+ * Adds the provided condition as the second argument of an OR
+ * clause to this node.
+ *
+ * @param condition Condition to 'OR' with.
+ * @return New top OR node, new root.
+ */
+ public ConditionsTreeNode or(SearchCondition condition) {
+ try {
+ ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+ return or(tmp);
+ } catch (Exception e) {
+ // impossible
+ return null;
+ }
+ }
+
+ /**
+ * This applies the MPTT labeling to the subtree of which this node
+ * is the root node.
+ *
+ * For a description on MPTT see:
+ * http://www.sitepoint.com/hierarchical-data-database-2/
+ */
+ public void applyMPTTLabel() {
+ applyMPTTLabel(1);
+ }
+
+
+ ///////////////////////////////////////////////////////////////
+ // Public accessors
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Returns the condition stored in this node.
+ * @return Condition stored in the node.
+ */
+ public SearchCondition getCondition() {
+ return mCondition;
+ }
+
+ /**
+ * Get a set of all the leaves in the tree.
+ * @return Set of all the leaves.
+ */
+ public HashSet getLeafSet() {
+ HashSet leafSet = new HashSet();
+ return getLeafSet(leafSet);
+ }
+
+ /**
+ * Returns a list of all the nodes in the subtree of which this node
+ * is the root. The list contains the nodes in a pre traversal order.
+ *
+ * @return List of all nodes in subtree in preorder.
+ */
+ public List preorder() {
+ ArrayList result = new ArrayList();
+ Stack stack = new Stack();
+ stack.push(this);
+
+ while (!stack.isEmpty()) {
+ ConditionsTreeNode current = stack.pop();
+
+ if (current.mLeft != null) {
+ stack.push(current.mLeft);
+ }
+
+ if (current.mRight != null) {
+ stack.push(current.mRight);
+ }
+
+ result.add(current);
+ }
+
+ return result;
+ }
+
+
+ ///////////////////////////////////////////////////////////////
+ // Private class logic
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Adds two new ConditionTreeNodes, one for the operator and one for the
+ * new condition. The current node will end up on the same level as the
+ * one provided in the arguments, they will be siblings. Their common
+ * parent node will be one containing the operator provided in the arguments.
+ * The method will update all the required references so the tree ends up in
+ * a valid state.
+ *
+ * This method only supports node arguments with a null parent node.
+ *
+ * @param Node to add.
+ * @param Operator that will connect the new node with this one.
+ * @return New parent node, containing the operator.
+ * @throws Exception Throws when the provided new node does not have a null parent.
+ */
+ private ConditionsTreeNode add(ConditionsTreeNode node, Operator op) throws Exception {
+ if (node.mParent != null) {
+ throw new Exception("Can only add new expressions from root node down.");
+ }
+
+ ConditionsTreeNode tmpNode = new ConditionsTreeNode(mParent, op);
+ tmpNode.mLeft = this;
+ tmpNode.mRight = node;
+
+ if (mParent != null) {
+ mParent.updateChild(this, tmpNode);
+ }
+
+ this.mParent = tmpNode;
+ node.mParent = tmpNode;
+
+ return tmpNode;
+ }
+
+ /**
+ * Helper method that replaces a child of the current node with a new node.
+ * If the provided old child node was the left one, left will be replaced with
+ * the new one. Same goes for the right one.
+ *
+ * @param oldChild Old child node to be replaced.
+ * @param newChild New child node.
+ */
+ private void updateChild(ConditionsTreeNode oldChild, ConditionsTreeNode newChild) {
+ // we can compare objects id's because this is the desired behaviour in this case
+ if (mLeft == oldChild) {
+ mLeft = newChild;
+ } else if (mRight == oldChild) {
+ mRight = newChild;
+ }
+ }
+
+ /**
+ * Recursive function to gather all the leaves in the subtree of which
+ * this node is the root.
+ *
+ * @param leafSet Leafset that's being built.
+ * @return Set of leaves being completed.
+ */
+ private HashSet getLeafSet(HashSet leafSet) {
+ if (mLeft == null && mRight == null) {
+ // if we ended up in a leaf, add ourself and return
+ leafSet.add(this);
+ return leafSet;
+ }
+
+ // we didn't end up in a leaf
+ if (mLeft != null) {
+ mLeft.getLeafSet(leafSet);
+ }
+
+ if (mRight != null) {
+ mRight.getLeafSet(leafSet);
+ }
+ return leafSet;
+ }
+
+ /**
+ * This applies the MPTT labeling to the subtree of which this node
+ * is the root node.
+ *
+ * For a description on MPTT see:
+ * http://www.sitepoint.com/hierarchical-data-database-2/
+ */
+ private int applyMPTTLabel(int label) {
+ mLeftMPTTMarker = label;
+
+ if (mLeft != null) {
+ label = mLeft.applyMPTTLabel(label += 1);
+ }
+
+ if (mRight != null) {
+ label = mRight.applyMPTTLabel(label += 1);
+ }
+
+ ++label;
+ mRightMPTTMarker = label;
+ return label;
+ }
+
+
+ ///////////////////////////////////////////////////////////////
+ // Parcelable
+ //
+ // This whole class has to be parcelable because it's passed
+ // on through intents.
+ ///////////////////////////////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mValue.ordinal());
+ dest.writeParcelable(mCondition, flags);
+ dest.writeParcelable(mLeft, flags);
+ dest.writeParcelable(mRight, flags);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public ConditionsTreeNode createFromParcel(Parcel in) {
+ return new ConditionsTreeNode(in);
+ }
+
+ @Override
+ public ConditionsTreeNode[] newArray(int size) {
+ return new ConditionsTreeNode[size];
+ }
+ };
+
+ private ConditionsTreeNode(Parcel in) {
+ mValue = Operator.values()[in.readInt()];
+ mCondition = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
+ mLeft = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
+ mRight = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
+ mParent = null;
+
+ if (mLeft != null) {
+ mLeft.mParent = this;
+ }
+
+ if (mRight != null) {
+ mRight.mParent = this;
+ }
+ }
+}
diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
new file mode 100644
index 000000000..a6041c41f
--- /dev/null
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -0,0 +1,428 @@
+package com.fsck.k9.search;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.fsck.k9.mail.Flag;
+
+/**
+ * This class represents a local search.
+ *
+ * Removing conditions could be done through matching there unique id in the leafset and then
+ * removing them from the tree.
+ *
+ * TODO implement a complete addAllowedFolder method
+ * TODO conflicting conditions check on add
+ * TODO duplicate condition checking?
+ * TODO assign each node a unique id that's used to retrieve it from the leaveset and remove.
+ *
+ */
+
+public class LocalSearch implements SearchSpecification {
+
+ private String mName;
+ private boolean mPredefined;
+ private boolean mManualSearch = false;
+
+ // since the uuid isn't in the message table it's not in the tree neither
+ private HashSet mAccountUuids = new HashSet();
+ private ConditionsTreeNode mConditions = null;
+ private HashSet mLeafSet = new HashSet();
+
+
+ ///////////////////////////////////////////////////////////////
+ // Constructors
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Use this only if the search won't be saved. Saved searches need
+ * a name!
+ */
+ public LocalSearch() {}
+
+ /**
+ *
+ * @param name
+ */
+ public LocalSearch(String name) {
+ this.mName = name;
+ }
+
+ /**
+ * Use this constructor when you know what you'r doing. Normally it's only used
+ * when restoring these search objects from the database.
+ *
+ * @param name Name of the search
+ * @param searchConditions SearchConditions, may contains flags and folders
+ * @param accounts Relative Account's uuid's
+ * @param predefined Is this a predefined search or a user created one?
+ */
+ protected LocalSearch(String name, ConditionsTreeNode searchConditions,
+ String accounts, boolean predefined) {
+ this(name);
+ mConditions = searchConditions;
+ mPredefined = predefined;
+ mLeafSet = new HashSet();
+ if (mConditions != null) {
+ mLeafSet.addAll(mConditions.getLeafSet());
+ }
+
+ // initialize accounts
+ if (accounts != null) {
+ for (String account : accounts.split(",")) {
+ mAccountUuids.add(account);
+ }
+ } else {
+ // impossible but still not unrecoverable
+ }
+ }
+
+ @Override
+ public LocalSearch clone() {
+ ConditionsTreeNode conditions = (mConditions == null) ? null : mConditions.cloneTree();
+
+ LocalSearch copy = new LocalSearch(mName, conditions, null, mPredefined);
+ copy.mManualSearch = mManualSearch;
+ copy.mAccountUuids = new HashSet(mAccountUuids);
+
+ return copy;
+ }
+
+ ///////////////////////////////////////////////////////////////
+ // Public manipulation methods
+ ///////////////////////////////////////////////////////////////
+ /**
+ * Sets the name of the saved search. If one existed it will
+ * be overwritten.
+ *
+ * @param name Name to be set.
+ */
+ public void setName(String name) {
+ this.mName = name;
+ }
+
+ /**
+ * Add a new account to the search. When no accounts are
+ * added manually we search all accounts on the device.
+ *
+ * @param uuid Uuid of the account to be added.
+ */
+ public void addAccountUuid(String uuid) {
+ if (uuid.equals(ALL_ACCOUNTS)) {
+ mAccountUuids.clear();
+ return;
+ }
+ mAccountUuids.add(uuid);
+ }
+
+ /**
+ * Adds all the account uuids in the provided array to
+ * be matched by the seach.
+ *
+ * @param accountUuids
+ */
+ public void addAccountUuids(String[] accountUuids) {
+ for (String acc : accountUuids) {
+ addAccountUuid(acc);
+ }
+ }
+
+ /**
+ * Removes an account UUID from the current search.
+ *
+ * @param uuid Account UUID to remove.
+ * @return True if removed, false otherwise.
+ */
+ public boolean removeAccountUuid(String uuid) {
+ return mAccountUuids.remove(uuid);
+ }
+
+ /**
+ * Adds the provided node as the second argument of an AND
+ * clause to this node.
+ *
+ * @param field Message table field to match against.
+ * @param string Value to look for.
+ * @param contains Attribute to use when matching.
+ *
+ * @throws IllegalConditionException
+ */
+ public void and(Searchfield field, String value, Attribute attribute) {
+ and(new SearchCondition(field, attribute, value));
+ }
+
+ /**
+ * Adds the provided condition as the second argument of an AND
+ * clause to this node.
+ *
+ * @param condition Condition to 'AND' with.
+ * @return New top AND node, new root.
+ */
+ public ConditionsTreeNode and(SearchCondition condition) {
+ try {
+ ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+ return and(tmp);
+ } catch (Exception e) {
+ // impossible
+ return null;
+ }
+ }
+
+ /**
+ * Adds the provided node as the second argument of an AND
+ * clause to this node.
+ *
+ * @param node Node to 'AND' with.
+ * @return New top AND node, new root.
+ * @throws Exception
+ */
+ public ConditionsTreeNode and(ConditionsTreeNode node) throws Exception {
+ mLeafSet.addAll(node.getLeafSet());
+
+ if (mConditions == null) {
+ mConditions = node;
+ return node;
+ }
+
+ mConditions = mConditions.and(node);
+ return mConditions;
+ }
+
+ /**
+ * Adds the provided condition as the second argument of an OR
+ * clause to this node.
+ *
+ * @param condition Condition to 'OR' with.
+ * @return New top OR node, new root.
+ */
+ public ConditionsTreeNode or(SearchCondition condition) {
+ try {
+ ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+ return or(tmp);
+ } catch (Exception e) {
+ // impossible
+ return null;
+ }
+ }
+
+ /**
+ * Adds the provided node as the second argument of an OR
+ * clause to this node.
+ *
+ * @param node Node to 'OR' with.
+ * @return New top OR node, new root.
+ * @throws Exception
+ */
+ public ConditionsTreeNode or(ConditionsTreeNode node) throws Exception {
+ mLeafSet.addAll(node.getLeafSet());
+
+ if (mConditions == null) {
+ mConditions = node;
+ return node;
+ }
+
+ mConditions = mConditions.or(node);
+ return mConditions;
+ }
+
+ /**
+ * Add all the flags to this node as required flags. The
+ * provided flags will be combined using AND with the root.
+ *
+ * @param requiredFlags Array of required flags.
+ */
+ public void allRequiredFlags(Flag[] requiredFlags) {
+ if (requiredFlags != null) {
+ for (Flag f : requiredFlags) {
+ and(new SearchCondition(Searchfield.FLAG, Attribute.CONTAINS, f.name()));
+ }
+ }
+ }
+
+ /**
+ * Add all the flags to this node as forbidden flags. The
+ * provided flags will be combined using AND with the root.
+ *
+ * @param forbiddenFlags Array of forbidden flags.
+ */
+ public void allForbiddenFlags(Flag[] forbiddenFlags) {
+ if (forbiddenFlags != null) {
+ for (Flag f : forbiddenFlags) {
+ and(new SearchCondition(Searchfield.FLAG, Attribute.NOT_CONTAINS, f.name()));
+ }
+ }
+ }
+
+ /**
+ * TODO
+ * FOR NOW: And the folder with the root.
+ *
+ * Add the folder as another folder to search in. The folder
+ * will be added AND to the root if no 'folder subtree' was found.
+ * Otherwise the folder will be added OR to that tree.
+ *
+ * @param name Name of the folder to add.
+ */
+ public void addAllowedFolder(String name) {
+ /*
+ * TODO find folder sub-tree
+ * - do and on root of it & rest of search
+ * - do or between folder nodes
+ */
+ mConditions = and(new SearchCondition(Searchfield.FOLDER, Attribute.EQUALS, name));
+ }
+
+ /*
+ * TODO make this more advanced!
+ * This is a temporarely solution that does NOT WORK for
+ * real searches because of possible extra conditions to a folder requirement.
+ */
+ public List getFolderNames() {
+ ArrayList results = new ArrayList();
+ for (ConditionsTreeNode node : mLeafSet) {
+ if (node.mCondition.field == Searchfield.FOLDER &&
+ node.mCondition.attribute == Attribute.EQUALS) {
+ results.add(node.mCondition.value);
+ }
+ }
+ return results;
+ }
+
+ /**
+ * Gets the leafset of the related condition tree.
+ *
+ * @return All the leaf conditions as a set.
+ */
+ public Set getLeafSet() {
+ return mLeafSet;
+ }
+
+ ///////////////////////////////////////////////////////////////
+ // Public accesor methods
+ ///////////////////////////////////////////////////////////////
+ /**
+ * TODO THIS HAS TO GO!!!!
+ * very dirty fix for remotesearch support atm
+ */
+ public String getRemoteSearchArguments() {
+ Set leafSet = getLeafSet();
+ if (leafSet == null) {
+ return null;
+ }
+
+ for (ConditionsTreeNode node : leafSet) {
+ if (node.getCondition().field == Searchfield.SUBJECT ||
+ node.getCondition().field == Searchfield.SENDER ) {
+ return node.getCondition().value;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns the name of the saved search.
+ *
+ * @return Name of the search.
+ */
+ @Override
+ public String getName() {
+ return (mName == null) ? "" : mName;
+ }
+
+ /**
+ * Checks if this search was hard coded and shipped with K-9
+ *
+ * @return True is search was shipped with K-9
+ */
+ public boolean isPredefined() {
+ return mPredefined;
+ }
+
+ public boolean isManualSearch() {
+ return mManualSearch;
+ }
+
+ public void setManualSearch(boolean manualSearch) {
+ mManualSearch = manualSearch;
+ }
+
+ /**
+ * Returns all the account uuids that this search will try to
+ * match against.
+ *
+ * @return Array of account uuids.
+ */
+ @Override
+ public String[] getAccountUuids() {
+ if (mAccountUuids.size() == 0) {
+ return new String[] { SearchSpecification.ALL_ACCOUNTS };
+ }
+
+ String[] tmp = new String[mAccountUuids.size()];
+ mAccountUuids.toArray(tmp);
+ return tmp;
+ }
+
+ /**
+ * Returns whether or not to search all accounts.
+ *
+ * @return {@code true} if all accounts should be searched.
+ */
+ public boolean searchAllAccounts() {
+ return (mAccountUuids.size() == 0);
+ }
+
+ /**
+ * Get the condition tree.
+ *
+ * @return The root node of the related conditions tree.
+ */
+ @Override
+ public ConditionsTreeNode getConditions() {
+ return mConditions;
+ }
+
+ ///////////////////////////////////////////////////////////////
+ // Parcelable
+ ///////////////////////////////////////////////////////////////
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mName);
+ dest.writeByte((byte) (mPredefined ? 1 : 0));
+ dest.writeByte((byte) (mManualSearch ? 1 : 0));
+ dest.writeStringList(new ArrayList(mAccountUuids));
+ dest.writeParcelable(mConditions, flags);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public LocalSearch createFromParcel(Parcel in) {
+ return new LocalSearch(in);
+ }
+
+ @Override
+ public LocalSearch[] newArray(int size) {
+ return new LocalSearch[size];
+ }
+ };
+
+ public LocalSearch(Parcel in) {
+ mName = in.readString();
+ mPredefined = (in.readByte() == 1);
+ mManualSearch = (in.readByte() == 1);
+ mAccountUuids.addAll(in.createStringArrayList());
+ mConditions = in.readParcelable(LocalSearch.class.getClassLoader());
+ mLeafSet = (mConditions == null) ? null : mConditions.getLeafSet();
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java
new file mode 100644
index 000000000..1eb20a7b5
--- /dev/null
+++ b/src/com/fsck/k9/search/SearchAccount.java
@@ -0,0 +1,91 @@
+package com.fsck.k9.search;
+
+import java.util.UUID;
+
+import android.content.Context;
+
+import com.fsck.k9.BaseAccount;
+import com.fsck.k9.R;
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+
+/**
+ * This class is basically a wrapper around a LocalSearch. It allows to expose it as
+ * an account. This is a meta-account containing all the e-mail that matches the search.
+ */
+public class SearchAccount implements BaseAccount {
+
+ // create the all messages search ( all accounts is default when none specified )
+ public static SearchAccount createAllMessagesAccount(Context context) {
+ String name = context.getString(R.string.search_all_messages_title);
+ LocalSearch tmpSearch = new LocalSearch(name);
+ return new SearchAccount(tmpSearch, name,
+ context.getString(R.string.search_all_messages_detail));
+ }
+
+
+ // create the unified inbox meta account ( all accounts is default when none specified )
+ public static SearchAccount createUnifiedInboxAccount(Context context) {
+ String name = context.getString(R.string.integrated_inbox_title);
+ LocalSearch tmpSearch = new LocalSearch(name);
+ tmpSearch.and(Searchfield.INTEGRATE, "1", Attribute.EQUALS);
+ return new SearchAccount(tmpSearch, name,
+ context.getString(R.string.integrated_inbox_detail));
+ }
+
+ private String mEmail;
+ private String mDescription;
+ private LocalSearch mSearch;
+ private String mFakeUuid;
+
+ public SearchAccount(LocalSearch search, String description, String email)
+ throws IllegalArgumentException {
+
+ if (search == null) {
+ throw new IllegalArgumentException("Provided LocalSearch was null");
+ }
+
+ mSearch = search;
+ mDescription = description;
+ mEmail = email;
+ }
+
+ @Override
+ public synchronized String getEmail() {
+ return mEmail;
+ }
+
+ @Override
+ public synchronized void setEmail(String email) {
+ this.mEmail = email;
+ }
+
+ @Override
+ public String getDescription() {
+ return mDescription;
+ }
+
+ @Override
+ public void setDescription(String description) {
+ this.mDescription = description;
+ }
+
+ public LocalSearch getRelatedSearch() {
+ return mSearch;
+ }
+
+ /*
+ * This will only be used when accessed as an Account. If that
+ * is the case we don't want to return the uuid of a real account since
+ * this is posing as a fake meta-account. If this object is accesed as
+ * a Search then methods from LocalSearch will be called which do handle
+ * things nice.
+ */
+ @Override
+ public String getUuid() {
+ if (mFakeUuid == null) {
+ mFakeUuid = UUID.randomUUID().toString();
+ }
+ return mFakeUuid;
+ }
+}
diff --git a/src/com/fsck/k9/search/SearchModifier.java b/src/com/fsck/k9/search/SearchModifier.java
new file mode 100644
index 000000000..22ec2740b
--- /dev/null
+++ b/src/com/fsck/k9/search/SearchModifier.java
@@ -0,0 +1,23 @@
+package com.fsck.k9.search;
+
+import com.fsck.k9.R;
+import com.fsck.k9.mail.Flag;
+
+/**
+ * This enum represents filtering parameters used by {@link com.fsck.k9.search.SearchAccount}.
+ */
+public enum SearchModifier {
+ FLAGGED(R.string.flagged_modifier, new Flag[] { Flag.FLAGGED }, null),
+ UNREAD(R.string.unread_modifier, null, new Flag[] { Flag.SEEN });
+
+ public final int resId;
+ public final Flag[] requiredFlags;
+ public final Flag[] forbiddenFlags;
+
+ SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) {
+ resId = nResId;
+ requiredFlags = nRequiredFlags;
+ forbiddenFlags = nForbiddenFlags;
+ }
+
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
new file mode 100644
index 000000000..1e599aed9
--- /dev/null
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -0,0 +1,163 @@
+package com.fsck.k9.search;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+public interface SearchSpecification extends Parcelable {
+
+ /**
+ * Get all the uuids of accounts this search acts on.
+ * @return Array of uuids.
+ */
+ public String[] getAccountUuids();
+
+ /**
+ * Returns the search's name if it was named.
+ * @return Name of the search.
+ */
+ public String getName();
+
+ /**
+ * Returns the root node of the condition tree accompanying
+ * the search.
+ *
+ * @return Root node of conditions tree.
+ */
+ public ConditionsTreeNode getConditions();
+
+ /*
+ * Some meta names for certain conditions.
+ */
+ public static final String ALL_ACCOUNTS = "allAccounts";
+
+ ///////////////////////////////////////////////////////////////
+ // ATTRIBUTE enum
+ ///////////////////////////////////////////////////////////////
+ public enum Attribute {
+ CONTAINS,
+ NOT_CONTAINS,
+
+ EQUALS,
+ NOT_EQUALS,
+
+ STARTSWITH,
+ NOT_STARTSWITH,
+
+ ENDSWITH,
+ NOT_ENDSWITH
+ }
+
+ ///////////////////////////////////////////////////////////////
+ // SEARCHFIELD enum
+ ///////////////////////////////////////////////////////////////
+ /*
+ * Using an enum in order to have more robust code. Users ( & coders )
+ * are prevented from passing illegal fields. No database overhead
+ * when invalid fields passed.
+ *
+ * By result, only the fields in here are searchable.
+ *
+ * Fields not in here at this moment ( and by effect not searchable ):
+ * id, html_content, internal_date, message_id,
+ * preview, mime_type
+ *
+ */
+ public enum Searchfield {
+ SUBJECT,
+ DATE,
+ UID,
+ FLAG,
+ SENDER,
+ TO,
+ CC,
+ FOLDER,
+ BCC,
+ REPLY_TO,
+ MESSAGE_CONTENTS,
+ ATTACHMENT_COUNT,
+ DELETED,
+ THREAD_ROOT,
+ ID,
+ INTEGRATE
+ }
+
+
+ ///////////////////////////////////////////////////////////////
+ // SearchCondition class
+ ///////////////////////////////////////////////////////////////
+ /**
+ * This class represents 1 value for a certain search field. One
+ * value consists of three things:
+ * an attribute: equals, starts with, contains,...
+ * a searchfield: date, flags, sender, subject,...
+ * a value: "apple", "jesse",..
+ *
+ * @author dzan
+ */
+ public class SearchCondition implements Parcelable {
+ public final String value;
+ public final Attribute attribute;
+ public final Searchfield field;
+
+ public SearchCondition(Searchfield field, Attribute attribute, String value) {
+ this.value = value;
+ this.attribute = attribute;
+ this.field = field;
+ }
+
+ private SearchCondition(Parcel in) {
+ this.value = in.readString();
+ this.attribute = Attribute.values()[in.readInt()];
+ this.field = Searchfield.values()[in.readInt()];
+ }
+
+ @Override
+ public SearchCondition clone() {
+ return new SearchCondition(field, attribute, value);
+ }
+
+ public String toHumanString() {
+ return field.toString() + attribute.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof SearchCondition) {
+ SearchCondition tmp = (SearchCondition) o;
+ if (tmp.attribute == attribute &&
+ tmp.field == field &&
+ tmp.value.equals(value)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(value);
+ dest.writeInt(attribute.ordinal());
+ dest.writeInt(field.ordinal());
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+
+ @Override
+ public SearchCondition createFromParcel(Parcel in) {
+ return new SearchCondition(in);
+ }
+
+ @Override
+ public SearchCondition[] newArray(int size) {
+ return new SearchCondition[size];
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
new file mode 100644
index 000000000..22d087ba0
--- /dev/null
+++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
@@ -0,0 +1,232 @@
+package com.fsck.k9.search;
+
+import java.util.List;
+
+import com.fsck.k9.Account;
+import com.fsck.k9.mail.MessagingException;
+import com.fsck.k9.mail.Folder.OpenMode;
+import com.fsck.k9.mail.store.LocalStore;
+import com.fsck.k9.mail.store.LocalStore.LocalFolder;
+import com.fsck.k9.search.SearchSpecification.SearchCondition;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
+
+
+public class SqlQueryBuilder {
+ public static void buildWhereClause(Account account, ConditionsTreeNode node,
+ StringBuilder query, List selectionArgs) {
+ buildWhereClauseInternal(account, node, query, selectionArgs);
+ }
+
+ private static void buildWhereClauseInternal(Account account, ConditionsTreeNode node,
+ StringBuilder query, List selectionArgs) {
+ if (node == null) {
+ return;
+ }
+
+ if (node.mLeft == null && node.mRight == null) {
+ SearchCondition condition = node.mCondition;
+ switch (condition.field) {
+ case FOLDER: {
+ String folderName = condition.value;
+ long folderId = getFolderId(account, folderName);
+ query.append("folder_id = ?");
+ selectionArgs.add(Long.toString(folderId));
+ break;
+ }
+ default: {
+ appendCondition(condition, query, selectionArgs);
+ }
+ }
+ } else {
+ query.append("(");
+ buildWhereClauseInternal(account, node.mLeft, query, selectionArgs);
+ query.append(") ");
+ query.append(node.mValue.name());
+ query.append(" (");
+ buildWhereClauseInternal(account, node.mRight, query, selectionArgs);
+ query.append(")");
+ }
+ }
+
+ private static void appendCondition(SearchCondition condition, StringBuilder query,
+ List selectionArgs) {
+ query.append(getColumnName(condition));
+ appendExprRight(condition, query, selectionArgs);
+ }
+
+ private static long getFolderId(Account account, String folderName) {
+ long folderId = 0;
+ try {
+ LocalStore localStore = account.getLocalStore();
+ LocalFolder folder = localStore.getFolder(folderName);
+ folder.open(OpenMode.READ_ONLY);
+ folderId = folder.getId();
+ } catch (MessagingException e) {
+ //FIXME
+ e.printStackTrace();
+ }
+
+ return folderId;
+ }
+
+ private static String getColumnName(SearchCondition condition) {
+ String columnName = null;
+ switch (condition.field) {
+ case ATTACHMENT_COUNT: {
+ columnName = "attachment_count";
+ break;
+ }
+ case BCC: {
+ columnName = "bcc_list";
+ break;
+ }
+ case CC: {
+ columnName = "cc_list";
+ break;
+ }
+ case DATE: {
+ columnName = "date";
+ break;
+ }
+ case DELETED: {
+ columnName = "deleted";
+ break;
+ }
+ case FLAG: {
+ columnName = "flags";
+ break;
+ }
+ case FOLDER: {
+ columnName = "folder_id";
+ break;
+ }
+ case ID: {
+ columnName = "id";
+ break;
+ }
+ case MESSAGE_CONTENTS: {
+ columnName = "text_content";
+ break;
+ }
+ case REPLY_TO: {
+ columnName = "reply_to_list";
+ break;
+ }
+ case SENDER: {
+ columnName = "sender_list";
+ break;
+ }
+ case SUBJECT: {
+ columnName = "subject";
+ break;
+ }
+ case THREAD_ROOT: {
+ columnName = "thread_root";
+ break;
+ }
+ case TO: {
+ columnName = "to_list";
+ break;
+ }
+ case UID: {
+ columnName = "uid";
+ break;
+ }
+ case INTEGRATE: {
+ columnName = "integrate";
+ break;
+ }
+ }
+
+ if (columnName == null) {
+ throw new RuntimeException("Unhandled case");
+ }
+
+ return columnName;
+ }
+
+ private static void appendExprRight(SearchCondition condition, StringBuilder query,
+ List selectionArgs) {
+ String value = condition.value;
+ Searchfield field = condition.field;
+
+ query.append(" ");
+ String selectionArg = null;
+ switch (condition.attribute) {
+ case NOT_CONTAINS:
+ query.append("NOT ");
+ //$FALL-THROUGH$
+ case CONTAINS: {
+ query.append("LIKE ?");
+ selectionArg = "%" + value + "%";
+ break;
+ }
+ case NOT_STARTSWITH:
+ query.append("NOT ");
+ //$FALL-THROUGH$
+ case STARTSWITH: {
+ query.append("LIKE ?");
+ selectionArg = "%" + value;
+ break;
+ }
+ case NOT_ENDSWITH:
+ query.append("NOT ");
+ //$FALL-THROUGH$
+ case ENDSWITH: {
+ query.append("LIKE ?");
+ selectionArg = value + "%";
+ break;
+ }
+ case NOT_EQUALS: {
+ if (isNumberColumn(field)) {
+ query.append("!= ?");
+ } else {
+ query.append("NOT LIKE ?");
+ }
+ selectionArg = value;
+ break;
+ }
+ case EQUALS: {
+ if (isNumberColumn(field)) {
+ query.append("= ?");
+ } else {
+ query.append("LIKE ?");
+ }
+ selectionArg = value;
+ break;
+ }
+ }
+
+ if (selectionArg == null) {
+ throw new RuntimeException("Unhandled case");
+ }
+
+ selectionArgs.add(selectionArg);
+ }
+
+ private static boolean isNumberColumn(Searchfield field) {
+ switch (field) {
+ case ATTACHMENT_COUNT:
+ case DATE:
+ case DELETED:
+ case FOLDER:
+ case ID:
+ case INTEGRATE:
+ case THREAD_ROOT: {
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) {
+ String result = selection;
+ for (String columnName : columnNames) {
+ result = result.replaceAll("\\b" + columnName + "\\b", prefix + columnName);
+ }
+
+ return result;
+ }
+}