From 63b6b497a06fddf57601c06f9be5bab3f8e49daa Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 8 Oct 2012 22:51:29 +0200 Subject: [PATCH 01/74] Early version of message threading Missing: - UI support for threading when polling - code to upgrade existing installations - UI elements to switch from/to threaded display mode - threading of messages with same subject --- res/layout/message_list_item.xml | 25 +- src/com/fsck/k9/activity/MessageList.java | 8 + .../k9/controller/MessagingController.java | 25 +- .../fsck/k9/fragment/MessageListFragment.java | 42 +- src/com/fsck/k9/helper/Utility.java | 42 ++ src/com/fsck/k9/mail/store/ImapStore.java | 2 +- src/com/fsck/k9/mail/store/LocalStore.java | 597 ++++++++++++++++-- 7 files changed, 684 insertions(+), 57 deletions(-) diff --git a/res/layout/message_list_item.xml b/res/layout/message_list_item.xml index 89c7bf619..14ce2996e 100644 --- a/res/layout/message_list_item.xml +++ b/res/layout/message_list_item.xml @@ -45,14 +45,32 @@ android:layout_height="wrap_content" android:layout_toRightOf="@+id/chip_wrapper" android:layout_below="@+id/subject" + android:layout_toLeftOf="@+id/thread_count" android:layout_marginLeft="1dip" android:layout_marginBottom="3dip" - android:layout_marginRight="16dip" - android:layout_alignParentRight="true" + android:layout_marginRight="3dip" android:bufferType="spannable" android:singleLine="false" android:textAppearance="?android:attr/textAppearanceSmall" android:textColor="?android:attr/textColorPrimary" /> + + + + - - - diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 8862314e9..eb7c91ee4 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -748,4 +748,12 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme return true; } + + @Override + public void showThread(Account account, String folderName, long threadRootId) { + MessageListFragment fragment = MessageListFragment.newInstance(account, folderName, + threadRootId); + + addMessageListFragment(fragment); + } } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index d5860a4c3..e4cf48734 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -517,13 +517,16 @@ public class MessagingController implements Runnable { * @param account * @param folder * @param listener + * @param threaded + * @param threadId * @throws MessagingException */ - public void listLocalMessages(final Account account, final String folder, final MessagingListener listener) { + public void listLocalMessages(final Account account, final String folder, + final MessagingListener listener, final boolean threaded, final long threadId) { threadPool.execute(new Runnable() { @Override public void run() { - listLocalMessagesSynchronous(account, folder, listener); + listLocalMessagesSynchronous(account, folder, listener, threaded, threadId); } }); } @@ -535,9 +538,12 @@ public class MessagingController implements Runnable { * @param account * @param folder * @param listener + * @param threaded + * @param threadId * @throws MessagingException */ - public void listLocalMessagesSynchronous(final Account account, final String folder, final MessagingListener listener) { + public void listLocalMessagesSynchronous(final Account account, final String folder, + final MessagingListener listener, boolean threaded, long threadId) { for (MessagingListener l : getListeners(listener)) { l.listLocalMessagesStarted(account, folder); @@ -588,10 +594,15 @@ public class MessagingController implements Runnable { //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 (threadId != -1) { + localFolder.getMessagesInThread(threadId, retrievalListener); + } else if (threaded) { + localFolder.getThreadedMessages(retrievalListener); + } else { + localFolder.getMessages(retrievalListener, false); + } + if (K9.DEBUG) Log.v(K9.LOG_TAG, "Got ack that callbackRunner finished"); diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index df8d85b33..87510e5a2 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -76,6 +76,7 @@ import com.fsck.k9.mail.Message; import com.fsck.k9.mail.Store; import com.fsck.k9.mail.store.LocalStore; import com.fsck.k9.mail.store.LocalStore.LocalFolder; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; @@ -94,6 +95,19 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return fragment; } + public static MessageListFragment newInstance(Account account, String folderName, + long threadRootId) { + MessageListFragment fragment = new MessageListFragment(); + + Bundle args = new Bundle(); + args.putString(ARG_ACCOUNT, account.getUuid()); + args.putString(ARG_FOLDER, folderName); + args.putLong(ARG_THREAD_ID, threadRootId); + fragment.setArguments(args); + + return fragment; + } + public static MessageListFragment newInstance(String title, String[] accountUuids, String[] folderNames, String queryString, Flag[] flags, Flag[] forbiddenFlags, boolean integrate) { @@ -290,6 +304,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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_THREAD_ID = "thread_id"; private static final String STATE_LIST_POSITION = "listPosition"; @@ -389,6 +404,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private DateFormat mTimeFormat; + //TODO: make this a setting + private boolean mThreadViewEnabled = true; + + private long mThreadId; + /** * This class is used to run operations that modify UI elements in the UI thread. @@ -667,6 +687,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); if (mSelectedCount > 0) { toggleMessageSelect(message); + } else if (((LocalMessage) message.message).getThreadCount() > 1) { + Folder folder = message.message.getFolder(); + long rootId = ((LocalMessage) message.message).getRootId(); + mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId); } else { onOpenMessage(message); } @@ -730,6 +754,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false); mSearchAccount = args.getString(ARG_SEARCH_ACCOUNT); mSearchFolder = args.getString(ARG_SEARCH_FOLDER); + mThreadId = args.getLong(ARG_THREAD_ID, -1); String accountUuid = args.getString(ARG_ACCOUNT); @@ -892,7 +917,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick //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); + mController.listLocalMessages(mAccount, mFolderName, mAdapter.mListener, mThreadViewEnabled, mThreadId); + // Hide the archive button if we don't have an archive folder. if (!mAccount.hasArchiveFolder()) { // mBatchArchiveButton.setVisibility(View.GONE); @@ -914,7 +940,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void run() { if (mFolderName != null) { - mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener); + mController.listLocalMessagesSynchronous(mAccount, mFolderName, mAdapter.mListener, mThreadViewEnabled, mThreadId); } else if (mQueryString != null) { mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener); } @@ -1929,6 +1955,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick holder.preview.setLines(mPreviewLines); holder.preview.setTextSize(TypedValue.COMPLEX_UNIT_SP, mFontSizes.getMessageListPreview()); + holder.threadCount = (TextView) view.findViewById(R.id.thread_count); view.setTag(holder); } @@ -2029,6 +2056,15 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick subject = message.message.getSubject(); } + int threadCount = ((LocalMessage) message.message).getThreadCount(); + if (threadCount > 1) { + holder.threadCount.setText(Integer.toString(threadCount)); + holder.threadCount.setVisibility(View.VISIBLE); + } else { + holder.threadCount.setVisibility(View.GONE); + } + + // We'll get badge support soon --jrv // if (holder.badge != null) { // String email = message.counterpartyAddress; @@ -2133,6 +2169,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public TextView date; public View chip; public int position = -1; + public TextView threadCount; @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -2902,6 +2939,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public interface MessageListFragmentListener { void setMessageListProgress(int level); + void showThread(Account account, String folderName, long rootId); void remoteSearch(String searchAccount, String searchFolder, String queryString); void showMoreFromSameSender(String senderAddress); void onResendMessage(Message message); diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java index 113032d34..5a66a0ab9 100644 --- a/src/com/fsck/k9/helper/Utility.java +++ b/src/com/fsck/k9/helper/Utility.java @@ -18,7 +18,9 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; +import java.util.ArrayList; import java.util.Date; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -666,4 +668,44 @@ 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; + } } 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..fe69d8af3 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -13,10 +13,8 @@ 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; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -82,11 +80,8 @@ 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[] EMPTY_FLAG_ARRAY = new Flag[0]; private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN, Flag.FLAGGED }; @@ -96,13 +91,13 @@ public class LocalStore extends Store implements Serializable { */ 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 "; + + "bcc_list, reply_to_list, attachment_count, internal_date, message_id, folder_id, preview, thread_root, thread_parent, empty "; 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"; - protected static final int DB_VERSION = 43; + protected static final int DB_VERSION = 44; protected String uUid = null; @@ -166,10 +161,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 +195,9 @@ 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)"); + + //TODO: add indices for the thread columns + 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 +383,18 @@ 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; + } + } + } } } catch (SQLiteException e) { Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0"); @@ -942,7 +973,7 @@ public class LocalStore extends Store implements Serializable { null, "SELECT " + GET_MESSAGES_COLS - + "FROM messages WHERE deleted = 0 " + whereClause.toString() + " ORDER BY date DESC" + + "FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 " + whereClause.toString() + " ORDER BY date DESC" , args.toArray(EMPTY_STRING_ARRAY) ); } @@ -1285,7 +1316,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 +1932,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,6 +1949,75 @@ public class LocalStore extends Store implements Serializable { } } + public Message[] getThreadedMessages(final MessageRetrievalListener listener) + throws MessagingException { + try { + return database.execute(false, new DbCallback() { + @Override + public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException, + UnavailableStorageException { + try { + open(OpenMode.READ_WRITE); + + return LocalStore.this.getMessages( + listener, + LocalFolder.this, + "SELECT " + + "m.subject, m.sender_list, MAX(m.date), m.uid, m.flags, " + + "m.id, m.to_list, m.cc_list, m.bcc_list, m.reply_to_list, " + + "m.attachment_count, m.internal_date, m.message_id, " + + "m.folder_id, m.preview, m.thread_root, m.thread_parent, " + + "m.empty, COUNT(h.id) " + + "FROM messages h JOIN messages m " + + "ON (h.id = m.thread_root OR h.id = m.id) " + + "WHERE h.deleted = 0 AND m.deleted = 0 AND " + + "(m.empty IS NULL OR m.empty != 1) AND " + + "h.thread_root IS NULL AND h.folder_id = ? " + + "GROUP BY h.id", + new String[] { Long.toString(mFolderId) }); + } catch (MessagingException e) { + throw new WrappedException(e); + } + } + }); + } catch (WrappedException e) { + throw(MessagingException) e.getCause(); + } + } + + public Message[] getMessagesInThread(final long threadId, + final MessageRetrievalListener listener) throws MessagingException { + try { + return database.execute(false, new DbCallback() { + @Override + public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException, + UnavailableStorageException { + try { + open(OpenMode.READ_WRITE); + + String threadIdString = Long.toString(threadId); + + return LocalStore.this.getMessages( + listener, + LocalFolder.this, + "SELECT " + GET_MESSAGES_COLS + " " + + "FROM messages " + + "WHERE deleted = 0 AND (empty IS NULL OR empty != 1) AND " + + "(thread_root = ? OR id = ?) AND folder_id = ?", + new String[] { + threadIdString, + threadIdString, + Long.toString(mFolderId) + }); + } catch (MessagingException e) { + throw new WrappedException(e); + } + } + }); + } catch (WrappedException e) { + throw(MessagingException) e.getCause(); + } + } @Override public Message[] getMessages(String[] uids, MessageRetrievalListener listener) @@ -1975,30 +2075,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); + int x = db.update("messages", cv, "thread_root = ?", oldIdArg); + + cv.clear(); + cv.put("thread_parent", id); + x = 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); @@ -2076,6 +2264,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 +2395,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 +2439,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 +2453,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) { @@ -2270,7 +2542,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)), @@ -2337,7 +2609,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; } }); @@ -2581,7 +2854,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) { @@ -2883,6 +3156,95 @@ 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 static class LocalTextBody extends TextBody { @@ -2930,6 +3292,11 @@ public class LocalStore extends Store implements Serializable { private boolean mHeadersLoaded = false; private boolean mMessageDirty = false; + private long mRootId; + private long mParentId; + private boolean mEmpty; + private int mThreadCount = 0; + public LocalMessage() { } @@ -2983,6 +3350,15 @@ 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); + + mEmpty = (cursor.isNull(17)) ? false : (cursor.getInt(17) == 1); + + if (cursor.getColumnCount() > 18) { + mThreadCount = cursor.getInt(18); + } } /** @@ -3236,13 +3612,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 +3642,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; } }); @@ -3262,12 +3651,12 @@ public class LocalStore extends Store implements Serializable { throw(MessagingException) e.getCause(); } ((LocalFolder)mFolder).deleteHeaders(mId); - - } /* * Completely remove a message from the local database + * + * TODO: document how this updates the thread structure */ @Override public void destroy() throws MessagingException { @@ -3277,9 +3666,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); } @@ -3385,6 +3867,22 @@ public class LocalStore extends Store implements Serializable { return message; } + + public long getRootId() { + return mRootId; + } + + public long getParentId() { + return mParentId; + } + + public boolean isEmpty() { + return mEmpty; + } + + public int getThreadCount() { + return mThreadCount; + } } public static class LocalAttachmentBodyPart extends MimeBodyPart { @@ -3449,4 +3947,19 @@ 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; + } + } + } From 16d23260339be29794ab8b353e81926bba6ad2ff Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Oct 2012 03:02:36 +0200 Subject: [PATCH 02/74] Nasty hack to retain threads in MessageListFragment when polling --- .../fsck/k9/activity/MessageInfoHolder.java | 1 + .../fsck/k9/fragment/MessageListFragment.java | 46 +++++++++++++++++-- src/com/fsck/k9/helper/MessageHelper.java | 3 ++ src/com/fsck/k9/mail/store/LocalStore.java | 22 +++++++-- 4 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageInfoHolder.java b/src/com/fsck/k9/activity/MessageInfoHolder.java index 4669daec6..56db97ced 100644 --- a/src/com/fsck/k9/activity/MessageInfoHolder.java +++ b/src/com/fsck/k9/activity/MessageInfoHolder.java @@ -24,6 +24,7 @@ public class MessageInfoHolder { public boolean selected; public String account; public String uri; + public int threadCount; // Empty constructor for comparison public MessageInfoHolder() { diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 87510e5a2..4bd5ba88f 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -687,7 +687,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); if (mSelectedCount > 0) { toggleMessageSelect(message); - } else if (((LocalMessage) message.message).getThreadCount() > 1) { + } else if (message.threadCount > 1) { Folder folder = message.message.getFolder(); long rootId = ((LocalMessage) message.message).getRootId(); mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId); @@ -1767,7 +1767,36 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick FolderInfoHolder folderInfoHolder = new FolderInfoHolder( getActivity(), messageFolder, messageAccount); messageHelper.populate(m, message, folderInfoHolder, messageAccount); - messagesToAdd.add(m); + + if (verifyAgainstSearch) { + LocalMessage localMessage = (LocalMessage) message; + + if (mThreadId != -1) { + if (localMessage.getRootId() == mThreadId || + localMessage.getId() == mThreadId) { + messagesToAdd.add(m); + } + } else if (mThreadViewEnabled) { + long threadId = localMessage.getRootId(); + if (threadId == -1) { + threadId = localMessage.getId(); + } + + MessageInfoHolder threadPlaceHolder = getThread(threadId); + if (threadPlaceHolder == null) { + messagesToAdd.add(m); + } else if (m.compareDate.after(threadPlaceHolder.compareDate)) { + messagesToRemove.add(threadPlaceHolder); + messagesToAdd.add(m); + } else { + threadPlaceHolder.threadCount = m.threadCount; + } + } else { + messagesToAdd.add(m); + } + } else { + messagesToAdd.add(m); + } } else { if (mQueryString != null) { if (verifyAgainstSearch) { @@ -1876,6 +1905,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return null; } + private MessageInfoHolder getThread(long threadId) { + for (MessageInfoHolder holder : mMessages) { + LocalMessage localMessage = (LocalMessage) holder.message; + if (localMessage.getId() == threadId || localMessage.getRootId() == threadId) { + return holder; + } + } + + return null; + } + public FolderInfoHolder getFolder(String folder, Account account) { LocalFolder local_folder = null; try { @@ -2056,7 +2096,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick subject = message.message.getSubject(); } - int threadCount = ((LocalMessage) message.message).getThreadCount(); + int threadCount = message.threadCount; if (threadCount > 1) { holder.threadCount.setText(Integer.toString(threadCount)); holder.threadCount.setVisibility(View.VISIBLE); diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java index 9c2abb491..ae708f224 100644 --- a/src/com/fsck/k9/helper/MessageHelper.java +++ b/src/com/fsck/k9/helper/MessageHelper.java @@ -17,6 +17,7 @@ import com.fsck.k9.mail.Flag; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.Message.RecipientType; +import com.fsck.k9.mail.store.LocalStore.LocalMessage; import com.fsck.k9.helper.DateFormatter; public class MessageHelper { @@ -86,6 +87,8 @@ public class MessageHelper { target.account = account.getUuid(); target.uri = "email://messages/" + account.getAccountNumber() + "/" + message.getFolder().getName() + "/" + message.getUid(); + target.threadCount = ((LocalMessage) message).getThreadCount(); + } catch (MessagingException me) { Log.w(K9.LOG_TAG, "Unable to load message info", me); } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index fe69d8af3..8c42b6f48 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -93,6 +93,10 @@ public class LocalStore extends Store implements Serializable { "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, thread_root, thread_parent, empty "; + static private String GET_MESSAGES_COLS_PREFIX = + "m.subject, m.sender_list, m.date, m.uid, m.flags, m.id, m.to_list, m.cc_list, " + + "m.bcc_list, m.reply_to_list, m.attachment_count, m.internal_date, m.message_id, " + + "m.folder_id, m.preview, m.thread_root, m.thread_parent, m.empty "; 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"; @@ -1891,13 +1895,23 @@ public class LocalStore extends Store implements Serializable { try { cursor = db.rawQuery( - "SELECT " - + GET_MESSAGES_COLS - + "FROM messages WHERE uid = ? AND folder_id = ?", + "SELECT " + + GET_MESSAGES_COLS_PREFIX + ", COUNT(c.id) " + + "FROM messages m LEFT JOIN messages c " + + "ON (" + + "(" + + "m.thread_root IN (c.thread_root, c.id)" + + " OR " + + "(m.thread_root IS NULL AND m.id IN (c.thread_root, c.id))" + + ") AND c.empty != 1) " + + "WHERE m.uid = ? AND m.folder_id = ?", new String[] { message.getUid(), Long.toString(mFolderId) }); - if (!cursor.moveToNext()) { + + // Note: Because of the COUNT(c.id) we will always get one result + // row even if nothing was found. So we check if 'id' is NULL + if (!cursor.moveToNext() || cursor.isNull(0)) { return null; } message.populateFromGetMessageCursor(cursor); From 3413cbebf54dd4c5f2b8738174dc9c81294d014a Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Oct 2012 04:30:19 +0200 Subject: [PATCH 03/74] Worked around NPEs when the activity has been detached --- .../fsck/k9/fragment/MessageListFragment.java | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 4bd5ba88f..d035460fd 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -410,6 +410,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private long mThreadId; + private Context mContext; + + /** * This class is used to run operations that modify UI elements in the UI thread. * @@ -634,7 +637,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() { @@ -700,6 +703,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) { @@ -1449,7 +1454,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void remoteSearchStarted(Account acct, String folder) { mHandler.progress(true); - mHandler.updateFooter(getString(R.string.remote_search_sending_query), true); + mHandler.updateFooter(mContext.getString(R.string.remote_search_sending_query), true); } @@ -1458,7 +1463,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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); + mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false); } else { mHandler.updateFooter("", false); } @@ -1470,9 +1475,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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); + mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading_limited, account.getRemoteSearchNumResults(), numResults), true); } else { - mHandler.updateFooter(getString(R.string.remote_search_downloading, numResults), true); + mHandler.updateFooter(mContext.getString(R.string.remote_search_downloading, numResults), true); } mFragmentListener.setMessageListProgress(Window.PROGRESS_START); } @@ -1765,7 +1770,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick if (updateForMe(account, folderName)) { m = new MessageInfoHolder(); FolderInfoHolder folderInfoHolder = new FolderInfoHolder( - getActivity(), messageFolder, messageAccount); + mContext, messageFolder, messageAccount); messageHelper.populate(m, message, folderInfoHolder, messageAccount); if (verifyAgainstSearch) { @@ -1804,7 +1809,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } else { m = new MessageInfoHolder(); FolderInfoHolder folderInfoHolder = new FolderInfoHolder( - getActivity(), messageFolder, messageAccount); + mContext, messageFolder, messageAccount); messageHelper.populate(m, message, folderInfoHolder, messageAccount); messagesToAdd.add(m); @@ -1813,7 +1818,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } else { m.dirty = false; // as we reload the message, unset its dirty flag - FolderInfoHolder folderInfoHolder = new FolderInfoHolder(getActivity(), + FolderInfoHolder folderInfoHolder = new FolderInfoHolder(mContext, messageFolder, account); messageHelper.populate(m, message, folderInfoHolder, account); needsSort = true; @@ -1921,7 +1926,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick try { LocalStore localStore = account.getLocalStore(); local_folder = localStore.getFolder(folder); - return new FolderInfoHolder(getActivity(), local_folder, account); + return new FolderInfoHolder(mContext, local_folder, account); } catch (Exception e) { Log.e(K9.LOG_TAG, "getFolder(" + folder + ") goes boom: ", e); return null; @@ -2241,17 +2246,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick if (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); From c2bb45171212048d867548be20c09dc6854d2ab7 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 10 Oct 2012 04:32:48 +0200 Subject: [PATCH 04/74] Don't display thread count in thread view --- src/com/fsck/k9/fragment/MessageListFragment.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index d035460fd..1fab56b86 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -2102,7 +2102,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } int threadCount = message.threadCount; - if (threadCount > 1) { + if (mThreadId == -1 && threadCount > 1) { holder.threadCount.setText(Integer.toString(threadCount)); holder.threadCount.setVisibility(View.VISIBLE); } else { From 611bae3fb4e8e572303b16c98a897493753a6381 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 12 Oct 2012 12:30:26 +0200 Subject: [PATCH 05/74] Created search package to hold the framework and moved over the SearchSpecification class ( refactor ). --- src/com/fsck/k9/K9.java | 4 ++-- src/com/fsck/k9/SearchAccount.java | 1 + src/com/fsck/k9/activity/Accounts.java | 2 +- src/com/fsck/k9/activity/FolderList.java | 2 +- src/com/fsck/k9/activity/LauncherShortcuts.java | 2 +- src/com/fsck/k9/activity/MessageList.java | 2 +- src/com/fsck/k9/controller/MessagingController.java | 2 +- src/com/fsck/k9/preferences/GlobalSettings.java | 4 ++-- src/com/fsck/k9/{ => search}/SearchSpecification.java | 2 +- 9 files changed, 11 insertions(+), 10 deletions(-) rename src/com/fsck/k9/{ => search}/SearchSpecification.java (91%) diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index e6caa157e..ef0f21fb0 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -579,8 +579,8 @@ public class K9 extends Application { public static void loadPrefs(Preferences prefs) { SharedPreferences sprefs = prefs.getPreferences(); - DEBUG = sprefs.getBoolean("enableDebugLogging", false); - DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); + DEBUG = sprefs.getBoolean("enableDebugLogging", true); + DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", true); mAnimations = sprefs.getBoolean("animations", true); mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", false); mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); diff --git a/src/com/fsck/k9/SearchAccount.java b/src/com/fsck/k9/SearchAccount.java index bf2f25570..97e2092ee 100644 --- a/src/com/fsck/k9/SearchAccount.java +++ b/src/com/fsck/k9/SearchAccount.java @@ -6,6 +6,7 @@ import java.util.UUID; import android.content.Context; import com.fsck.k9.mail.Flag; +import com.fsck.k9.search.SearchSpecification; /** * This is a meta-Account that represents one or more accounts with filters on them. The filter specification diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index f45fe52e5..cbaaaa8ef 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -63,7 +63,6 @@ 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; @@ -80,6 +79,7 @@ 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.SearchSpecification; import com.fsck.k9.view.ColorChip; import com.fsck.k9.preferences.SettingsExporter; import com.fsck.k9.preferences.SettingsImportExportException; diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index fa2deb5c9..dc034c4ba 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,7 @@ 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.SearchSpecification; import com.fsck.k9.service.MailService; /** diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java index 415689057..add1d837a 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 diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index eb7c91ee4..d984efdbe 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -24,7 +24,6 @@ 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; @@ -35,6 +34,7 @@ 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.SearchSpecification; /** diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index e4cf48734..bc67559ae 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -40,7 +40,6 @@ 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; @@ -70,6 +69,7 @@ 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.SearchSpecification; /** diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java index e49face93..019391003 100644 --- a/src/com/fsck/k9/preferences/GlobalSettings.java +++ b/src/com/fsck/k9/preferences/GlobalSettings.java @@ -64,10 +64,10 @@ public class GlobalSettings { new V(1, new DateFormatSetting(DateFormatter.DEFAULT_FORMAT)) )); s.put("enableDebugLogging", Settings.versions( - new V(1, new BooleanSetting(false)) + new V(1, new BooleanSetting(true)) )); s.put("enableSensitiveLogging", Settings.versions( - new V(1, new BooleanSetting(false)) + new V(1, new BooleanSetting(true)) )); s.put("fontSizeAccountDescription", Settings.versions( new V(1, new FontSizeSetting(FontSizes.SMALL)) diff --git a/src/com/fsck/k9/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java similarity index 91% rename from src/com/fsck/k9/SearchSpecification.java rename to src/com/fsck/k9/search/SearchSpecification.java index a1f1de0a8..e08032ff5 100644 --- a/src/com/fsck/k9/SearchSpecification.java +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -1,5 +1,5 @@ -package com.fsck.k9; +package com.fsck.k9.search; import com.fsck.k9.mail.Flag; From 5c6552cbf3462c3f57b7f092dd4198bda4b5ca52 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Fri, 12 Oct 2012 13:22:54 +0200 Subject: [PATCH 06/74] Adding the 3 core classes for the search framework. ConditionsTreeNode, LocalSearch and SearchSpecification. --- .../fsck/k9/search/ConditionsTreeNode.java | 365 +++++++++++++++++ src/com/fsck/k9/search/LocalSearch.java | 374 ++++++++++++++++++ .../fsck/k9/search/SearchSpecification.java | 191 ++++++++- 3 files changed, 917 insertions(+), 13 deletions(-) create mode 100644 src/com/fsck/k9/search/ConditionsTreeNode.java create mode 100644 src/com/fsck/k9/search/LocalSearch.java diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java new file mode 100644 index 000000000..fbb31a7b9 --- /dev/null +++ b/src/com/fsck/k9/search/ConditionsTreeNode.java @@ -0,0 +1,365 @@ +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 + * + * @author dzan + */ +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; + } + + + /////////////////////////////////////////////////////////////// + // 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); + } + + /** + * 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); + } + + /** + * 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; + } + + + /** + * This will traverse the tree inorder and call toString recursively resulting + * in a valid SQL where clause. + */ + @Override + public String toString() { + return (mLeft == null ? "" : "(" + mLeft + ")") + + " " + ( mCondition == null ? mValue.name() : mCondition ) + " " + + (mRight == null ? "" : "(" + mRight + ")") ; + } + + /** + * 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 we ended up in a leaf, add ourself and return + if (mLeft == null && mRight == null) { + leafSet.add(this); + return leafSet; + // we didn't end up in a leaf + } else { + 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() { + public ConditionsTreeNode createFromParcel(Parcel in) { + return new ConditionsTreeNode(in); + } + + 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..48ca3ca7c --- /dev/null +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -0,0 +1,374 @@ +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. + * + * @author dzan + * + * 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; + + // 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 + } + } + + + /////////////////////////////////////////////////////////////// + // 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(); + } + 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) { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return and(tmp); + } + + /** + * 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. + */ + public ConditionsTreeNode and(ConditionsTreeNode node) { + try { + mLeafSet.add(node); + + if (mConditions == null) { + mConditions = node; + return node; + } + + mConditions = mConditions.and(node); + return mConditions; + } catch (Exception e) { + // IMPOSSIBLE! + return null; + } + } + + /** + * 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) { + ConditionsTreeNode tmp = new ConditionsTreeNode(condition); + return or(tmp); + } + + /** + * 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. + */ + public ConditionsTreeNode or(ConditionsTreeNode node) { + try { + mLeafSet.add(node); + + if (mConditions == null) { + mConditions = node; + return node; + } + + mConditions = mConditions.or(node); + return mConditions; + } catch (Exception e) { + // IMPOSSIBLE! + return null; + } + } + + /** + * 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 + */ + 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 + /////////////////////////////////////////////////////////////// + /** + * Returns the name of the saved search. + * + * @return Name of the search. + */ + public String getName() { + return 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; + } + + /** + * 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; + } + + /** + * 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.writeStringList(new ArrayList(mAccountUuids)); + dest.writeParcelable(mConditions, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public LocalSearch createFromParcel(Parcel in) { + return new LocalSearch(in); + } + + public LocalSearch[] newArray(int size) { + return new LocalSearch[size]; + } + }; + + public LocalSearch(Parcel in) { + mName = in.readString(); + mPredefined = in.readByte() == 1; + mAccountUuids.addAll(in.createStringArrayList()); + mConditions = in.readParcelable(LocalSearch.class.getClassLoader()); + mLeafSet = mConditions.getLeafSet(); + } +} \ No newline at end of file diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java index e08032ff5..3c422c64e 100644 --- a/src/com/fsck/k9/search/SearchSpecification.java +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -1,19 +1,184 @@ - package com.fsck.k9.search; -import com.fsck.k9.mail.Flag; - -public interface SearchSpecification { - - public Flag[] getRequiredFlags(); - - public Flag[] getForbiddenFlags(); - - public boolean isIntegrate(); - - public String getQuery(); +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"; + public static final String GENERIC_INBOX_NAME = "genericInboxName"; + + /////////////////////////////////////////////////////////////// + // ATTRIBUTE enum + /////////////////////////////////////////////////////////////// + public enum ATTRIBUTE { + CONTAINS(false), EQUALS(false), STARTSWITH(false), ENDSWITH(false), + NOT_CONTAINS(true), NOT_EQUALS(true), NOT_STARTSWITH(true), NOT_ENDSWITH(true); + + private boolean mNegation; + + private ATTRIBUTE(boolean negation) { + this.mNegation = negation; + } + + public String formQuery(String value) { + String queryPart = ""; + + switch (this) { + case NOT_CONTAINS: + case CONTAINS: + queryPart = "'%"+value+"%'"; + break; + case NOT_EQUALS: + case EQUALS: + queryPart = "'"+value+"'"; + break; + case NOT_STARTSWITH: + case STARTSWITH: + queryPart = "'%"+value+"'"; + break; + case NOT_ENDSWITH: + case ENDSWITH: + queryPart = "'"+value+"%'"; + break; + default: queryPart = "'"+value+"'"; + } + + return (mNegation ? " NOT LIKE " : " LIKE ") + queryPart; + } + }; + + /////////////////////////////////////////////////////////////// + // 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("subject"), DATE("date"), UID("uid"), FLAG("flags"), + SENDER("sender_list"), TO("to_list"), CC("cc_list"), FOLDER("folder_id"), + BCC("bcc_list"), REPLY_TO("reply_to_list"), MESSAGE("text_content"), + ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"); - public String[] getFolderNames(); + private String dbName; + + private SEARCHFIELD(String dbName) { + this.dbName = dbName; + } + + public String getDatabaseName() { + return dbName; + } + } + + + /////////////////////////////////////////////////////////////// + // 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 String value; + public ATTRIBUTE attribute; + public 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()]; + } + + public String toHumanString() { + return field.toString() + attribute.toString(); + } + + @Override + public String toString() { + return field.getDatabaseName() + attribute.formQuery(value); + } + + @Override + public boolean equals(Object o) { + if (o instanceof SearchCondition) { + SearchCondition tmp = (SearchCondition) o; + if (tmp.attribute == attribute + && tmp.value.equals(value) + && tmp.field == field) { + return true; + } else { + return false; + } + } else { + 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() { + public SearchCondition createFromParcel(Parcel in) { + return new SearchCondition(in); + } + + public SearchCondition[] newArray(int size) { + return new SearchCondition[size]; + } + }; + } } \ No newline at end of file From d27f909600219f5442f1650eb2b74700ab1af292 Mon Sep 17 00:00:00 2001 From: Sander Bogaert Date: Sat, 13 Oct 2012 08:53:00 -0400 Subject: [PATCH 07/74] Add new search logic to the MessagingController and LocalStore classes. --- .../k9/controller/MessagingController.java | 160 ++++-------------- src/com/fsck/k9/mail/store/LocalStore.java | 155 ++++++++--------- 2 files changed, 98 insertions(+), 217 deletions(-) diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index bc67559ae..dfb638828 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -69,6 +69,7 @@ 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; @@ -619,136 +620,39 @@ public class MessagingController implements Runnable { } } - 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())) { - 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 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); - } - - } - + 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; + } + + // 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(); @@ -760,22 +664,18 @@ public class MessagingController implements Runnable { listener.listLocalMessagesAddMessages(account, null, messages); } } - - } - @Override - public void messagesFinished(int number) { - } }; - + + // alert everyone the search has started + if (listener != null) { + listener.listLocalMessagesStarted(account, null); + } + + // build and do the query in the localstore 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); - + LocalStore localStore = account.getLocalStore(); + localStore.searchForMessages(retrievalListener, search); } catch (Exception e) { if (listener != null) { listener.listLocalMessagesFailed(account, null, e.getMessage()); @@ -786,7 +686,9 @@ public class MessagingController implements Runnable { listener.listLocalMessagesFinished(account, null); } } - } + } + + // publish the total search statistics if (listener != null) { listener.searchStats(stats); } diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 8c42b6f48..40893169e 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -69,6 +69,9 @@ 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.search.ConditionsTreeNode; +import com.fsck.k9.search.LocalSearch; +import com.fsck.k9.search.SearchSpecification.SEARCHFIELD; /** *
@@ -658,6 +661,22 @@ public class LocalStore extends Store implements Serializable {
         return new LocalFolder(name);
     }
 
+    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  getPersonalNamespaces(boolean forceListAll) throws MessagingException {
@@ -890,97 +909,57 @@ 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 ");
+    // TODO find beter solution
+    private static boolean isFolderId(String str) {
+        if (str == null) {
+                return false;
+        }
+        int length = str.length();
+        if (length == 0) {
+                return false;
+        }
+        int i = 0;
+        if (str.charAt(0) == '-') {
+        	return false;
+        }
+        for (; i < length; i++) {
+                char c = str.charAt(i);
+                if (c <= '/' || c >= ':') {
+                        return false;
                 }
-                whereClause.append(queryField).append(" LIKE ? ");
-                args.add(likeString);
-                anyAdded = true;
-            }
-
-
-            whereClause.append(" )");
         }
-        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(" )");
-        }
-        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(" )");
-        }
-        if (forbiddenFlags != null && forbiddenFlags.length > 0) {
-            whereClause.append(" AND (");
-            boolean anyAdded = false;
-            for (Flag flag : forbiddenFlags) {
-                if (anyAdded) {
-                    whereClause.append(" AND ");
-                }
-                anyAdded = true;
-                whereClause.append(" flags NOT LIKE ?");
-
-                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 ?");
-
-                args.add("%" + flag.toString() + "%");
-            }
-            whereClause.append(" )");
-        }
-
-        if (K9.DEBUG) {
-            Log.v(K9.LOG_TAG, "whereClause = " + whereClause.toString());
-            Log.v(K9.LOG_TAG, "args = " + args);
-        }
-        return getMessages(
-                   listener,
-                   null,
-                   "SELECT "
-                   + GET_MESSAGES_COLS
-                   + "FROM messages WHERE (empty IS NULL OR empty != 1) AND deleted = 0 " + whereClause.toString() + " ORDER BY date DESC"
-                   , args.toArray(EMPTY_STRING_ARRAY)
-               );
+        return true;
     }
+    
+	public Message[] searchForMessages(MessageRetrievalListener retrievalListener,
+										LocalSearch search) throws MessagingException {  
+		
+		// update some references in the search that have to be bound to this one store
+		for (ConditionsTreeNode node : search.getLeafSet()) {
+			if (node.mCondition.field == SEARCHFIELD.FOLDER) {	
+				// TODO find better solution
+				if (isFolderId(node.mCondition.value)) {
+					continue;
+				}
+				
+				if (node.mCondition.value.equals(LocalSearch.GENERIC_INBOX_NAME)) {
+					node.mCondition.value = mAccount.getInboxFolderName();
+				}
+				node.mCondition.value = String.valueOf(getFolderId(node.mCondition.value));
+			}
+		}
+
+    	// build sql query       
+        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE deleted = 0 " 
+        				+ (search.getConditions() != null ? "AND (" + search.getConditions() + ")" : "") + " ORDER BY date DESC";
+        
+        if (K9.DEBUG) {
+            Log.d(K9.LOG_TAG, "Query = " + sqlQuery);
+        }
+        
+		return getMessages(retrievalListener, null, sqlQuery, new String[] {});
+	}
+	
     /*
      * Given a query string, actually do the query for the messages and
      * call the MessageRetrievalListener for each one

From 442805fe6237d530c8fab9a4af563acee816b877 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 09:28:19 -0400
Subject: [PATCH 08/74] Made thread_root a searchable field of the message
 table. This can be used to display threads.

---
 src/com/fsck/k9/search/SearchSpecification.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
index 3c422c64e..6f6a38fa5 100644
--- a/src/com/fsck/k9/search/SearchSpecification.java
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -90,7 +90,7 @@ public interface SearchSpecification extends Parcelable {
         SUBJECT("subject"), DATE("date"), UID("uid"), FLAG("flags"),
         SENDER("sender_list"), TO("to_list"), CC("cc_list"), FOLDER("folder_id"),
         BCC("bcc_list"), REPLY_TO("reply_to_list"), MESSAGE("text_content"),
-        ATTACHMENT_COUNT("attachment_count"), DELETED("deleted");
+        ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"), THREAD_ROOT("thread_root");
 
         private String dbName;
         

From 9883148b2f48f7854d2871ffc3d827f942f89f81 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 10:03:19 -0400
Subject: [PATCH 09/74] Fixed wrong construction of leaf sets and unrightfully
 ignored exceptions.

---
 src/com/fsck/k9/search/LocalSearch.java | 64 ++++++++++++-------------
 1 file changed, 32 insertions(+), 32 deletions(-)

diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 48ca3ca7c..6b07459c4 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -153,8 +153,13 @@ public class LocalSearch implements SearchSpecification {
 	 * @return New top AND node, new root.
 	 */
 	public ConditionsTreeNode and(SearchCondition condition) {
-		ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
-		return and(tmp);
+		try {
+			ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+			return and(tmp);
+		} catch (Exception e) {
+			// impossible
+			return null;
+		}
 	}
 	
 	/**
@@ -163,22 +168,17 @@ public class LocalSearch implements SearchSpecification {
 	 * 
 	 * @param node Node to 'AND' with.
 	 * @return New top AND node, new root.
+	 * @throws Exception 
 	 */
-	public ConditionsTreeNode and(ConditionsTreeNode node) {
-		try {
-			mLeafSet.add(node);
-			
-			if (mConditions == null) {
-				mConditions = node;
-				return node;
-			}
-			
-			mConditions = mConditions.and(node);
-			return mConditions;
-		} catch (Exception e) {
-			// IMPOSSIBLE!
-			return null;
+	public ConditionsTreeNode and(ConditionsTreeNode node) throws Exception {
+		mLeafSet.addAll(node.getLeafSet());
+		
+		if (mConditions == null) {
+			mConditions = node;
+			return node;
 		}
+		
+		return mConditions.and(node);
 	}
 	
 	/**
@@ -189,8 +189,13 @@ public class LocalSearch implements SearchSpecification {
 	 * @return New top OR node, new root.
 	 */
 	public ConditionsTreeNode or(SearchCondition condition) {
-		ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
-		return or(tmp);
+		try {
+			ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
+			return or(tmp);
+		} catch (Exception e) {
+			// impossible
+			return null;
+		}
 	}
 	
 	/**
@@ -199,22 +204,17 @@ public class LocalSearch implements SearchSpecification {
 	 * 
 	 * @param node Node to 'OR' with.
 	 * @return New top OR node, new root.
+	 * @throws Exception 
 	 */
-	public ConditionsTreeNode or(ConditionsTreeNode node) {
-		try {
-			mLeafSet.add(node);
-			
-			if (mConditions == null) {
-				mConditions = node;
-				return node;
-			}
-			
-			mConditions = mConditions.or(node);
-			return mConditions;
-		} catch (Exception e) {
-			// IMPOSSIBLE!
-			return null;
+	public ConditionsTreeNode or(ConditionsTreeNode node) throws Exception {
+		mLeafSet.addAll(node.getLeafSet());
+		
+		if (mConditions == null) {
+			mConditions = node;
+			return node;
 		}
+		
+		return mConditions.or(node);
 	}
 	
 	/**

From bdfc9d685283b99ecd7cc76502401d621c8a4865 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 10:08:39 -0400
Subject: [PATCH 10/74] Added two convenience methods to avoid dealing with
 exceptions all the time.

---
 .../fsck/k9/search/ConditionsTreeNode.java    | 36 +++++++++++++++++++
 1 file changed, 36 insertions(+)

diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java
index fbb31a7b9..650755a7d 100644
--- a/src/com/fsck/k9/search/ConditionsTreeNode.java
+++ b/src/com/fsck/k9/search/ConditionsTreeNode.java
@@ -141,6 +141,24 @@ public class ConditionsTreeNode implements Parcelable{
 		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.
@@ -153,6 +171,24 @@ public class ConditionsTreeNode implements Parcelable{
 		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.

From 235e1f913b8431a0d0efe080c29ebc0e1ca92456 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 10:40:13 -0400
Subject: [PATCH 11/74] Avoid needless nullpointers.

---
 src/com/fsck/k9/search/LocalSearch.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 6b07459c4..a9bff7348 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -298,7 +298,7 @@ public class LocalSearch implements SearchSpecification {
      * @return Name of the search.
      */
     public String getName() {
-        return mName;
+        return (mName == null ? "" : mName);
     }
 	
     /**

From f550aaefb58bb41ae4b5a358a63b07a7dccc273e Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 14:03:40 -0400
Subject: [PATCH 12/74] Add new SearchAccount class and move it together with
 SearchModifier to the search package ( refactor ).

---
 src/com/fsck/k9/SearchAccount.java            | 153 ------------------
 src/com/fsck/k9/activity/AccountList.java     |   2 +-
 src/com/fsck/k9/activity/FolderList.java      |   1 +
 src/com/fsck/k9/provider/MessageProvider.java |   2 +-
 src/com/fsck/k9/search/SearchAccount.java     |  66 ++++++++
 .../{activity => search}/SearchModifier.java  |   4 +-
 6 files changed, 71 insertions(+), 157 deletions(-)
 delete mode 100644 src/com/fsck/k9/SearchAccount.java
 create mode 100644 src/com/fsck/k9/search/SearchAccount.java
 rename src/com/fsck/k9/{activity => search}/SearchModifier.java (90%)

diff --git a/src/com/fsck/k9/SearchAccount.java b/src/com/fsck/k9/SearchAccount.java
deleted file mode 100644
index 97e2092ee..000000000
--- a/src/com/fsck/k9/SearchAccount.java
+++ /dev/null
@@ -1,153 +0,0 @@
-package com.fsck.k9;
-
-import java.io.Serializable;
-import java.util.UUID;
-
-import android.content.Context;
-
-import com.fsck.k9.mail.Flag;
-import com.fsck.k9.search.SearchSpecification;
-
-/**
- * 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/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/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index dc034c4ba..e4c2a3e2a 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -67,6 +67,7 @@ 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.SearchModifier;
 import com.fsck.k9.search.SearchSpecification;
 import com.fsck.k9.service.MailService;
 
diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 1e351d057..05428b6cb 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -21,7 +21,6 @@ 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;
@@ -34,6 +33,7 @@ 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;
diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java
new file mode 100644
index 000000000..d58c62de1
--- /dev/null
+++ b/src/com/fsck/k9/search/SearchAccount.java
@@ -0,0 +1,66 @@
+package com.fsck.k9.search;
+
+import java.util.UUID;
+
+import com.fsck.k9.BaseAccount;
+
+/**
+ * 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 {
+
+    private String mEmail = null;
+    private String mDescription = null;
+    private LocalSearch mSearch = null;
+    private String mFakeUuid = null;
+    
+    public SearchAccount(LocalSearch search, String description, String email) throws IllegalArgumentException{
+    	if (search == null) {
+    		throw new IllegalArgumentException("Provided LocalSearch was null");
+    	}
+    	
+    	this.mSearch = search;
+    	this.mDescription = description;
+    	this.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;
+    }
+    
+    @Override
+    /*
+     * 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.
+     */
+    public String getUuid() {
+    	if (mFakeUuid == null){
+    		mFakeUuid = UUID.randomUUID().toString();
+    	}
+    	return mFakeUuid;
+    }
+}
diff --git a/src/com/fsck/k9/activity/SearchModifier.java b/src/com/fsck/k9/search/SearchModifier.java
similarity index 90%
rename from src/com/fsck/k9/activity/SearchModifier.java
rename to src/com/fsck/k9/search/SearchModifier.java
index 8b610ff6e..536fee51a 100644
--- a/src/com/fsck/k9/activity/SearchModifier.java
+++ b/src/com/fsck/k9/search/SearchModifier.java
@@ -1,10 +1,10 @@
-package com.fsck.k9.activity;
+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.SearchAccount}.
+ * This enum represents filtering parameters used by {@link com.fsck.k9.search.SearchAccount}.
  */
 enum SearchModifier {
     FLAGGED(R.string.flagged_modifier, new Flag[]{Flag.FLAGGED}, null),

From a3d227649fc0ec8bb8f4290bbec8857cddafea80 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 14:12:52 -0400
Subject: [PATCH 13/74] Change visibility of SearchModifier class and members.

---
 src/com/fsck/k9/search/SearchModifier.java | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/com/fsck/k9/search/SearchModifier.java b/src/com/fsck/k9/search/SearchModifier.java
index 536fee51a..f027ab062 100644
--- a/src/com/fsck/k9/search/SearchModifier.java
+++ b/src/com/fsck/k9/search/SearchModifier.java
@@ -6,13 +6,13 @@ import com.fsck.k9.mail.Flag;
 /**
  * This enum represents filtering parameters used by {@link com.fsck.k9.search.SearchAccount}.
  */
-enum SearchModifier {
+public 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;
+    public final int resId;
+    public final Flag[] requiredFlags;
+    public final Flag[] forbiddenFlags;
 
     SearchModifier(int nResId, Flag[] nRequiredFlags, Flag[] nForbiddenFlags) {
         resId = nResId;

From f01f2f15cde94825288db6cd6fe9f314b0c47ab1 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 14:19:50 -0400
Subject: [PATCH 14/74] Add static methods to create unifiedInbox and
 allMessages accounts.

---
 src/com/fsck/k9/search/SearchAccount.java | 21 +++++++++++++++++++++
 1 file changed, 21 insertions(+)

diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java
index d58c62de1..d76a8a0dd 100644
--- a/src/com/fsck/k9/search/SearchAccount.java
+++ b/src/com/fsck/k9/search/SearchAccount.java
@@ -2,7 +2,10 @@ package com.fsck.k9.search;
 
 import java.util.UUID;
 
+import android.content.Context;
+
 import com.fsck.k9.BaseAccount;
+import com.fsck.k9.R;
 
 /**
  * This class is basically a wrapper around a LocalSearch. It allows to expose it as
@@ -10,6 +13,24 @@ import com.fsck.k9.BaseAccount;
  */
 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.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME);
+        return new SearchAccount(tmpSearch, name,
+        		context.getString(R.string.integrated_inbox_detail));
+	}
+	
     private String mEmail = null;
     private String mDescription = null;
     private LocalSearch mSearch = null;

From bf82d0af7cbb17ab1c2c2e74fffe3748c7a77e05 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 15:06:57 -0400
Subject: [PATCH 15/74] Changed all the calls to MessageList to work using the
 new LocalSearch class. These are all tested and working changes.

---
 src/com/fsck/k9/activity/Accounts.java        |  80 ++++++--------
 src/com/fsck/k9/activity/FolderList.java      | 100 +++++-------------
 .../fsck/k9/activity/LauncherShortcuts.java   |   3 +-
 .../k9/controller/MessagingController.java    |  14 ++-
 src/com/fsck/k9/provider/MessageProvider.java |   2 +-
 .../k9/provider/UnreadWidgetProvider.java     |   7 +-
 6 files changed, 76 insertions(+), 130 deletions(-)

diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index cbaaaa8ef..596801554 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -62,7 +62,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.SearchAccount;
 import com.fsck.k9.activity.misc.ExtendedAsyncTask;
 import com.fsck.k9.activity.misc.NonConfigurationInstance;
 import com.fsck.k9.activity.setup.AccountSettings;
@@ -79,6 +78,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.search.SearchSpecification;
 import com.fsck.k9.view.ColorChip;
 import com.fsck.k9.preferences.SettingsExporter;
@@ -424,8 +426,18 @@ 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);
+    	// create the unified inbox meta account ( all accounts is default when none specified )
+        String name = getString(R.string.integrated_inbox_title);
+        LocalSearch tmpSearch = new LocalSearch(name);
+        tmpSearch.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME);
+        integratedInboxAccount = new SearchAccount(tmpSearch, name,
+        		getString(R.string.integrated_inbox_detail));
+        
+        // create the all messages search ( all accounts is default when none specified )
+        name = getString(R.string.search_all_messages_title);
+        tmpSearch = new LocalSearch(name);
+        unreadAccount = new SearchAccount(tmpSearch, name, 
+        		getString(R.string.search_all_messages_detail)); 	
     }
 
     @SuppressWarnings("unchecked")
@@ -550,7 +562,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 +620,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);
         } else {
             Account realAccount = (Account)account;
             if (!realAccount.isEnabled()) {
@@ -624,8 +637,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, true);}
         }
         return true;
     }
@@ -1769,49 +1784,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();
+            	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, false);
         }
 
     }
diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java
index e4c2a3e2a..6c8477833 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -67,6 +67,7 @@ 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;
@@ -621,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);
     }
 
     private void onCompact(Account account) {
@@ -1258,86 +1262,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, 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 add1d837a..f302051dc 100644
--- a/src/com/fsck/k9/activity/LauncherShortcuts.java
+++ b/src/com/fsck/k9/activity/LauncherShortcuts.java
@@ -31,8 +31,7 @@ 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, true, true);
         } else {
             shortcutIntent = FolderList.actionHandleAccountIntent(this, (Account) account, null,
                     true);
diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index dfb638828..181cb0177 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -3160,8 +3160,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, true, true);
+        
         PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
         builder.setContentIntent(pi);
 
@@ -3243,8 +3246,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, true, true);
+        
         PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
         builder.setContentIntent(pi);
 
diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 05428b6cb..d40ed23e0 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -302,7 +302,7 @@ 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();
diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
index 2af479d15..d8daeaa0b 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,10 @@ 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, true, true);
                 }
                 clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
             }

From 49197b4f1c3d599d0af773559e59fbc267d08286 Mon Sep 17 00:00:00 2001
From: Sander Bogaert 
Date: Sat, 13 Oct 2012 15:07:28 -0400
Subject: [PATCH 16/74] First attempt at making the fragmented messagelist and
 remote search play nice with the new search framework. Works partially.

---
 src/com/fsck/k9/activity/MessageList.java     | 249 ++++------
 .../fsck/k9/fragment/MessageListFragment.java | 436 ++++++++----------
 src/com/fsck/k9/search/LocalSearch.java       |  14 +
 3 files changed, 294 insertions(+), 405 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index d984efdbe..aca636bbd 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -34,7 +34,11 @@ 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 +48,36 @@ import com.fsck.k9.search.SearchSpecification;
  */
 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";
+
+    // 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 newTask) {
+        actionDisplaySearch(context, search, newTask, true);
     }
-
-    public static Intent actionHandleFolderIntent(Context context, Account account, String folder) {
+    
+    public static void actionDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) {
+        context.startActivity(intentDisplaySearch(context, search, newTask, clearTop));
+    }
+    
+    public static Intent intentDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) {
         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);
+        intent.putExtra(EXTRA_SEARCH, search);
+        
+        if (clearTop) {
+        	intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
         }
+        if (newTask) {
+        	intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        }
+        
         return intent;
     }
 
-    public static void actionHandle(Context context, String title, String queryString, boolean integrate, Flag[] flags, Flag[] forbiddenFlags) {
-        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);
-    }
-
-    /**
-     * 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 (searchSpecification.getForbiddenFlags() != null) {
-            intent.putExtra(EXTRA_FORBIDDEN_FLAGS, Utility.combine(searchSpecification.getForbiddenFlags(), ','));
-        }
-        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 +85,22 @@ 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;
+    private boolean mIsRemote;
+    
     @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 +116,56 @@ 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, mIsRemote);
             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
                 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
                 if (appData != null) {
-                    mSearchAccount = appData.getString(EXTRA_SEARCH_ACCOUNT);
-                    mSearchFolder = appData.getString(EXTRA_SEARCH_FOLDER);
+                    mSearch = new LocalSearch();
+                    mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT));
+                    mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER));
+                    
+                    String query = intent.getStringExtra(SearchManager.QUERY);
+                    mSearch.or(new SearchCondition(SEARCHFIELD.SENDER, ATTRIBUTE.CONTAINS, query));
+                    mSearch.or(new SearchCondition(SEARCHFIELD.SUBJECT, ATTRIBUTE.CONTAINS, query));
+                    
+                    mIsRemote = true;
                 }
-            } else {
-                mSearchAccount = intent.getStringExtra(EXTRA_SEARCH_ACCOUNT);
-                mSearchFolder = intent.getStringExtra(EXTRA_SEARCH_FOLDER);
             }
+        } else {
+        	// regular LocalSearch object was passed
+        	mSearch = intent.getParcelableExtra(EXTRA_SEARCH);      	
         }
 
-        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;
-        }
-
-        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();
-        }
+	    String[] accounts = mSearch.getAccountUuids();
+	    mSingleAccountMode = ( accounts != null && accounts.length == 1 
+	    		&& !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS));
+	    mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1);
+	    
+	    if (mSingleAccountMode) {
+		    mAccount = Preferences.getPreferences(this).getAccount(accounts[0]);
+		    
+		    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);
+	    }
+	    
+	    // now we know if we are in single account mode and need a subtitle
+	    mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE);
     }
 
     @Override
@@ -276,10 +201,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,17 +328,11 @@ 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.
-                    onBackPressed();
-                } else {
-                    // In a standard message list of a folder.  Go to folder list.
-                    onShowFolderList();
-                }
+                } else if (!mSingleFolderMode) {
+            		onBackPressed();
+            	} else {
+            		onShowFolderList();
+            	}
                 return true;
             }
             case R.id.compose: {
@@ -470,7 +385,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;
@@ -534,7 +449,7 @@ 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);
@@ -666,8 +581,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);
     }
@@ -716,8 +634,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
     @Override
     public void remoteSearch(String searchAccount, String searchFolder, String queryString) {
-        MessageListFragment fragment = MessageListFragment.newInstance(searchAccount, searchFolder,
-                queryString, true);
+        MessageListFragment fragment = MessageListFragment.newInstance(mSearch, true);
 
         addMessageListFragment(fragment);
     }
@@ -751,9 +668,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
     @Override
     public void showThread(Account account, String folderName, long threadRootId) {
-        MessageListFragment fragment = MessageListFragment.newInstance(account, folderName,
-                threadRootId);
-
+    	LocalSearch tmpSearch = new LocalSearch();
+    	tmpSearch.addAccountUuids(mSearch.getAccountUuids());
+    	tmpSearch.and(SEARCHFIELD.THREAD_ROOT, String.valueOf(threadRootId), ATTRIBUTE.EQUALS);
+    	
+        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
         addMessageListFragment(fragment);
     }
 }
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 1fab56b86..3932fed90 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -77,6 +77,12 @@ import com.fsck.k9.mail.Store;
 import com.fsck.k9.mail.store.LocalStore;
 import com.fsck.k9.mail.store.LocalStore.LocalFolder;
 import com.fsck.k9.mail.store.LocalStore.LocalMessage;
+import com.fsck.k9.search.ConditionsTreeNode;
+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;
 import com.handmark.pulltorefresh.library.PullToRefreshBase;
 import com.handmark.pulltorefresh.library.PullToRefreshListView;
 
@@ -84,67 +90,14 @@ import com.handmark.pulltorefresh.library.PullToRefreshListView;
 public class MessageListFragment extends SherlockFragment implements OnItemClickListener,
         ConfirmationDialogFragmentListener {
 
-    public static MessageListFragment newInstance(Account account, String folderName) {
+	public static MessageListFragment newInstance(LocalSearch search, boolean remoteSearch) {
         MessageListFragment fragment = new MessageListFragment();
-
         Bundle args = new Bundle();
-        args.putString(ARG_ACCOUNT, account.getUuid());
-        args.putString(ARG_FOLDER, folderName);
-        fragment.setArguments(args);
-
-        return fragment;
-    }
-
-    public static MessageListFragment newInstance(Account account, String folderName,
-            long threadRootId) {
-        MessageListFragment fragment = new MessageListFragment();
-
-        Bundle args = new Bundle();
-        args.putString(ARG_ACCOUNT, account.getUuid());
-        args.putString(ARG_FOLDER, folderName);
-        args.putLong(ARG_THREAD_ID, threadRootId);
-        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.putParcelable(ARG_SEARCH, search);
         args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
         fragment.setArguments(args);
-
-        return fragment;
-    }
-
+        return fragment;	
+	}
 
     /**
      * Reverses the result of a {@link Comparator}.
@@ -292,20 +245,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     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_THREAD_ID = "thread_id";
-
+    private static final String ARG_SEARCH = "searchObject";
+    private static final String ARG_REMOTE_SEARCH = "remoteSearch";
     private static final String STATE_LIST_POSITION = "listPosition";
 
     /**
@@ -356,18 +297,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     /**
      * 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 Future mRemoteSearchFuture = null;
-    private boolean mIntegrate = false;
-    private String[] mAccountUuids = null;
-    private String[] mFolderNames = null;
+    
     private String mTitle;
-
+    private LocalSearch mSearch = null;
+    private boolean mSingleAccountMode;
+    private boolean mSingleFolderMode;
+    
     private MessageListHandler mHandler = new MessageListHandler();
 
     private SortType mSortType = SortType.SORT_DATE;
@@ -596,7 +533,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private void setWindowTitle() {
         // regular folder content display
-        if (mFolderName != null) {
+        if (mSingleFolderMode) {
             Activity activity = getActivity();
             String displayName = FolderInfoHolder.getDisplayName(activity, mAccount,
                 mFolderName);
@@ -609,7 +546,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             } 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.
@@ -626,8 +563,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 {
@@ -665,15 +602,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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) {
+            } else if (mRemoteSearch && mAdapter.mExtraSearchResults != null && mAdapter.mExtraSearchResults.size() > 0 && mAccount != null) {
                 int numResults = mAdapter.mExtraSearchResults.size();
                 Context appContext = getActivity().getApplicationContext();
-                Account account = Preferences.getPreferences(appContext).getAccount(mSearchAccount);
-                if (account == null) {
+                if (mAccount == null) {
                     mHandler.updateFooter("", false);
                     return;
                 }
-                int limit = account.getRemoteSearchNumResults();
+                int limit = mAccount.getRemoteSearchNumResults();
                 List toProcess = mAdapter.mExtraSearchResults;
                 if (limit > 0 && numResults > limit) {
                     toProcess = toProcess.subList(0, limit);
@@ -682,7 +618,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                     mAdapter.mExtraSearchResults = null;
                     mHandler.updateFooter("", false);
                 }
-                mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener);
+                mController.loadSearchResults(mAccount, mCurrentFolder.name, toProcess, mAdapter.mListener);
             }
             return;
         }
@@ -754,56 +690,58 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     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);
-        mThreadId = args.getLong(ARG_THREAD_ID, -1);
-
-        String accountUuid = args.getString(ARG_ACCOUNT);
-
-        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]);
-            }
-        }
-
-        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]);
-            }
-        }
-
-        mIntegrate = args.getBoolean(ARG_INTEGRATE, false);
-        mAccountUuids = args.getStringArray(ARG_ACCOUNT_UUIDS);
-        mFolderNames = args.getStringArray(ARG_FOLDER_NAMES);
-        mTitle = args.getString(ARG_TITLE);
+        mSearch = args.getParcelable(ARG_SEARCH);
+        mTitle = args.getString(mSearch.getName());
+        
+        Context appContext = getActivity().getApplicationContext();  
+	    String[] accounts = mSearch.getAccountUuids();
+	    
+	    mSingleAccountMode = false;
+	    if (accounts != null && accounts.length == 1 
+	    		&& !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
+	    	mSingleAccountMode = true;
+		    mAccount = Preferences.getPreferences(appContext).getAccount(accounts[0]);
+	    }
+	    
+	    mSingleFolderMode = false;
+	    if (mSingleAccountMode && (mSearch.getFolderNames().size() == 1)) {
+	    	mSingleFolderMode = true;
+	    	mFolderName = mSearch.getFolderNames().get(0);
+	    	mCurrentFolder = getFolder(mFolderName, mAccount);
+	    }
     }
 
     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);
+        
+	    // Hide "Load up to x more" footer for search views
+	    mFooterView.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE);
 
         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();
+            }
+        }
+    }
+    
     @Override
     public void onPause() {
         super.onPause();
@@ -851,14 +789,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         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 =
@@ -872,24 +802,26 @@ 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 (!mAccount.allowRemoteSearch()) {
+	            mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
+	                @Override
+	                public void onRefresh(PullToRefreshBase refreshView) {
+	                    checkMail();
+	                }
+	            });
+	        // TODO this has to go! find better remote search integration
+        	} else {
+                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.setMode(PullToRefreshBase.Mode.DISABLED);
         }
@@ -899,7 +831,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         //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 };
@@ -918,9 +850,40 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (mAdapter.isEmpty()) {
+        	mController.searchLocalMessages(mSearch, mAdapter.mListener);
             if (mRemoteSearch) {
                 //TODO: Support flag based search
-                mRemoteSearchFuture = mController.searchRemoteMessages(mSearchAccount, mSearchFolder, mQueryString, null, null, mAdapter.mListener);
+               /* mRemoteSearchFuture = mController.searchRemoteMessages(mAccount.getUuid(), mCurrentFolder.name, mSearch.getRemoteSearchArguments(),
+                			null, null, mAdapter.mListener);*/
+            }
+        } else {
+            // reread the selected date format preference in case it has changed
+            mMessageHelper.refresh();
+            mAdapter.markAllMessagesAsDirty();
+  
+            new Thread() {
+                @Override
+                public void run() {
+                	mController.searchLocalMessages(mSearch, mAdapter.mListener);
+
+                    mHandler.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mAdapter.pruneDirtyMessages();
+                            mAdapter.notifyDataSetChanged();
+                            restoreListState();
+                        }
+                    });
+                }
+
+            }.start();
+        }
+        
+        /*if (mAdapter.isEmpty()) {
+            if (mRemoteSearch) {
+                //TODO: Support flag based search
+                mRemoteSearchFuture = mController.searchRemoteMessages(mAccount.getUuid(), mCurrentFolder.name, mSearch.getRemoteSearchArguments(),
+                			null, null, mAdapter.mListener);
             } else if (mFolderName != null) {
                 mController.listLocalMessages(mAccount, mFolderName,  mAdapter.mListener, mThreadViewEnabled, mThreadId);
 
@@ -928,8 +891,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 if (!mAccount.hasArchiveFolder()) {
 //                    mBatchArchiveButton.setVisibility(View.GONE);
                 }
-            } else if (mQueryString != null) {
-                mController.searchLocalMessages(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener);
+            } else {
+                mController.searchLocalMessages(mSearch, mAdapter.mListener);
                 // Don't show the archive button if this is a search.
 //                mBatchArchiveButton.setVisibility(View.GONE);
             }
@@ -944,11 +907,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 new Thread() {
                     @Override
                     public void run() {
-                        if (mFolderName != null) {
-                            mController.listLocalMessagesSynchronous(mAccount, mFolderName,  mAdapter.mListener, mThreadViewEnabled, mThreadId);
-                        } else if (mQueryString != null) {
-                            mController.searchLocalMessagesSynchronous(mAccountUuids, mFolderNames, null, mQueryString, mIntegrate, mQueryFlags, mForbiddenFlags, mAdapter.mListener);
-                        }
+                    	mController.searchLocalMessages(mSearch, mAdapter.mListener);
 
                         mHandler.post(new Runnable() {
                             @Override
@@ -963,7 +922,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 }
                 .start();
             }
-        }
+        }*/
 
         if (mAccount != null && mFolderName != null && !mRemoteSearch) {
             mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener);
@@ -989,7 +948,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     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.
@@ -1023,22 +982,15 @@ 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);
+        mFragmentListener.remoteSearch(searchAccount, searchFolder, mSearch.getRemoteSearchArguments());
     }
 
     /**
@@ -1055,7 +1007,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         mSortType = sortType;
 
         Preferences prefs = Preferences.getPreferences(getActivity().getApplicationContext());
-        Account account = getCurrentAccount(prefs);
+        Account account = mAccount;
 
         if (account != null) {
             account.setSortType(mSortType);
@@ -1247,7 +1199,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
         }
 
-        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;
@@ -1531,7 +1483,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             @Override
             public void listLocalMessagesStarted(Account account, String folder) {
-                if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
+                if ((!mSingleAccountMode && folder == null) || (account != null && account.equals(mAccount))) {
                     mHandler.progress(true);
                     if (folder != null) {
                         mHandler.folderLoading(folder, true);
@@ -1541,7 +1493,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             @Override
             public void listLocalMessagesFailed(Account account, String folder, String message) {
-                if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
+                if ((!mSingleAccountMode && folder == null) || (account != null && account.equals(mAccount))) {
                     mHandler.sortMessages();
                     mHandler.progress(false);
                     if (folder != null) {
@@ -1552,7 +1504,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             @Override
             public void listLocalMessagesFinished(Account account, String folder) {
-                if ((mQueryString != null && folder == null) || (account != null && account.equals(mAccount))) {
+                if ((!mSingleAccountMode && folder == null) || (account != null && account.equals(mAccount))) {
                     mHandler.sortMessages();
                     mHandler.progress(false);
                     if (folder != null) {
@@ -1601,12 +1553,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             }
         };
 
-        private boolean updateForMe(Account account, String folder) {
+        private boolean updateForMe(Account account, String folder) {     	
+        	// TODO get a contentprovider :D
+        	return true;
+        	
+        	/*
             if ((account.equals(mAccount) && mFolderName != null && folder.equals(mFolderName))) {
                 return true;
             } else {
                 return false;
             }
+            */
         }
 
         public List getMessages() {
@@ -1726,7 +1683,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         public void resetUnreadCount() {
-            if (mQueryString != null) {
+            if (!mSingleFolderMode) {
                 int unreadCount = 0;
 
                 for (MessageInfoHolder holder : mMessages) {
@@ -1802,7 +1759,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                             } else {
                                 messagesToAdd.add(m);
                             }
-                        } else {
+                        } /*else {
                             if (mQueryString != null) {
                                 if (verifyAgainstSearch) {
                                     messagesToSearch.add(message);
@@ -1815,7 +1772,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                                     messagesToAdd.add(m);
                                 }
                             }
-                        }
+                        }*/
                     } else {
                         m.dirty = false; // as we reload the message, unset its dirty flag
                         FolderInfoHolder folderInfoHolder = new FolderInfoHolder(mContext,
@@ -1827,9 +1784,29 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             }
 
             if (!messagesToSearch.isEmpty()) {
-                mController.searchLocalMessages(mAccountUuids, mFolderNames,
-                        messagesToSearch.toArray(EMPTY_MESSAGE_ARRAY), mQueryString, mIntegrate,
-                        mQueryFlags, mForbiddenFlags,
+            	// building a tree with all possible message id's we want to search in
+            	ConditionsTreeNode msgIdTree = new ConditionsTreeNode(
+            			new SearchCondition(SEARCHFIELD.UID, ATTRIBUTE.EQUALS, 
+            					String.valueOf(messagesToSearch.get(0).getUid())));
+            	
+            	if (messagesToSearch.size() > 1) {
+            		for(int i=1; i
Date: Tue, 16 Oct 2012 15:46:40 +0200
Subject: [PATCH 17/74] Switched to ContentProvider and CursorLoader to display
 the message list

---
 AndroidManifest.xml                           |    5 +
 .../fsck/k9/fragment/MessageListFragment.java | 1872 +++++++----------
 src/com/fsck/k9/helper/MessageHelper.java     |   24 +
 src/com/fsck/k9/mail/store/LocalStore.java    |    3 +
 .../fsck/k9/mail/store/LockableDatabase.java  |    2 +-
 src/com/fsck/k9/provider/EmailProvider.java   |  246 +++
 6 files changed, 1053 insertions(+), 1099 deletions(-)
 create mode 100644 src/com/fsck/k9/provider/EmailProvider.java

diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index d6f8aa48a..2a951fb0e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -402,6 +402,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"
             />
+        
 
          {
+
+    private static final String[] 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,
+        MessageColumns.THREAD_PARENT
+    };
+
+    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 THREAD_PARENT_COLUMN = 13;
+
 
     public static MessageListFragment newInstance(Account account, String folderName) {
         MessageListFragment fragment = new MessageListFragment();
@@ -283,11 +323,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     }
 
-    /**
-     * 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;
@@ -376,6 +411,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private boolean mSenderAboveSubject = false;
 
     private int mSelectedCount = 0;
+    private SparseBooleanArray mSelected;
 
     private FontSizes mFontSizes = K9.getFontSizes();
 
@@ -391,7 +427,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * chosen messages between user interactions (eg. Selecting a folder for
      * move operation)
      */
-    private List mActiveMessages;
+    private Message[] mActiveMessages;
 
     /* package visibility for faster inner class access */
     MessageHelper mMessageHelper;
@@ -412,6 +448,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private Context mContext;
 
+    private final ActivityListener mListener = new MessageListActivityListener();
 
     /**
      * This class is used to run operations that modify UI elements in the UI thread.
@@ -423,20 +460,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_SORT_MESSAGES = 1; + private static final int ACTION_FOLDER_LOADING = 2; + private static final int ACTION_REFRESH_TITLE = 3; + private static final int ACTION_PROGRESS = 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); @@ -469,43 +498,9 @@ 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) { 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; @@ -581,10 +576,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; } @@ -603,7 +598,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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 { @@ -664,13 +659,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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) { + mController.loadMoreMessages(mAccount, mFolderName, null); + } /*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); + updateFooter("", false); return; } int limit = account.getRemoteSearchNumResults(); @@ -680,22 +675,26 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mAdapter.mExtraSearchResults = mAdapter.mExtraSearchResults.subList(limit, mAdapter.mExtraSearchResults.size()); } else { mAdapter.mExtraSearchResults = null; - mHandler.updateFooter("", false); + updateFooter("", false); } mController.loadSearchResults(account, mSearchFolder, toProcess, mAdapter.mListener); - } + }*/ return; } - final MessageInfoHolder message = (MessageInfoHolder) parent.getItemAtPosition(position); + Cursor cursor = (Cursor) parent.getItemAtPosition(position); if (mSelectedCount > 0) { - toggleMessageSelect(message); - } else if (message.threadCount > 1) { - Folder folder = message.message.getFolder(); - long rootId = ((LocalMessage) message.message).getRootId(); - mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId); + toggleMessageSelect(position); +// } else if (message.threadCount > 1) { +// Folder folder = message.message.getFolder(); +// long rootId = ((LocalMessage) message.message).getRootId(); +// mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId); } else { - onOpenMessage(message); + MessageReference ref = new MessageReference(); + ref.accountUuid = mAccount.getUuid(); + ref.folderName = mCurrentFolder.name; + ref.uid = cursor.getString(UID_COLUMN); + onOpenMessage(ref); } } @@ -749,6 +748,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mMessageHelper = MessageHelper.getInstance(getActivity()); initializeMessageList(); + + getLoaderManager().initLoader(0, null, this); } private void decodeArguments() { @@ -794,7 +795,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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 @@ -804,10 +805,26 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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(); + } + } + } + @Override public void onPause() { super.onPause(); - mController.removeListener(mAdapter.mListener); + mController.removeListener(mListener); saveListState(); } @@ -894,7 +911,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mPullToRefreshView.setMode(PullToRefreshBase.Mode.DISABLED); } - mController.addListener(mAdapter.mListener); + mController.addListener(mListener); //Cancel pending new mail notifications when we open an account Account[] accountsWithNotification; @@ -917,6 +934,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mController.notifyAccountCancel(appContext, accountWithNotification); } + /* if (mAdapter.isEmpty()) { if (mRemoteSearch) { //TODO: Support flag based search @@ -964,9 +982,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick .start(); } } + */ if (mAccount != null && mFolderName != null && !mRemoteSearch) { - mController.getFolderUnreadMessageCount(mAccount, mFolderName, mAdapter.mListener); + mController.getFolderUnreadMessageCount(mAccount, mFolderName, mListener); } refreshTitle(); @@ -980,12 +999,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mListView.setScrollingCacheEnabled(false); mListView.setOnItemClickListener(this); mListView.addFooterView(getFooterView(mListView)); + //mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE); registerForContextMenu(mListView); } - private void onOpenMessage(MessageInfoHolder message) { - mFragmentListener.openMessage(message.message.makeMessageReference()); + private void onOpenMessage(MessageReference reference) { + mFragmentListener.openMessage(reference); } public void onCompose() { @@ -1000,20 +1020,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) { @@ -1117,17 +1137,12 @@ 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); - } - mAdapter.removeMessages(holders); - mController.deleteMessages(messagesToRemove.toArray(EMPTY_MESSAGE_ARRAY), null); + private void onDelete(Message message) { + onDelete(new Message[] { message }); + } + + private void onDelete(Message[] messages) { + mController.deleteMessages(messages, null); } @Override @@ -1144,22 +1159,22 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER); - final List holders = mActiveMessages; + final Message[] 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[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; } } @@ -1184,7 +1199,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick case R.id.dialog_confirm_spam: { String title = getString(R.string.dialog_confirm_spam_title); - int selectionSize = mActiveMessages.size(); + int selectionSize = mActiveMessages.length; String message = getResources().getQuantityString( R.plurals.dialog_confirm_spam_message, selectionSize, Integer.valueOf(selectionSize)); @@ -1242,7 +1257,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick return true; } case R.id.select_all: { - setSelectionState(true); + selectAll(); return true; } } @@ -1271,110 +1286,144 @@ 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()); + //TODO: get account from cursor + Account account = mAccount; - 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); } @@ -1413,209 +1462,155 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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) { + //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(mContext.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) { + 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); + 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 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 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) { + 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(mContext.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(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); - 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; @@ -1623,576 +1618,33 @@ 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; - } + //TODO: implement - 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(); - } + //notifyDataSetChanged(); } public void sortMessages() { - final Comparator chainComparator = getComparator(); + //TODO: implement - Collections.sort(mMessages, chainComparator); - - notifyDataSetChanged(); + //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( - mContext, messageFolder, messageAccount); - messageHelper.populate(m, message, folderInfoHolder, messageAccount); - - if (verifyAgainstSearch) { - LocalMessage localMessage = (LocalMessage) message; - - if (mThreadId != -1) { - if (localMessage.getRootId() == mThreadId || - localMessage.getId() == mThreadId) { - messagesToAdd.add(m); - } - } else if (mThreadViewEnabled) { - long threadId = localMessage.getRootId(); - if (threadId == -1) { - threadId = localMessage.getId(); - } - - MessageInfoHolder threadPlaceHolder = getThread(threadId); - if (threadPlaceHolder == null) { - messagesToAdd.add(m); - } else if (m.compareDate.after(threadPlaceHolder.compareDate)) { - messagesToRemove.add(threadPlaceHolder); - messagesToAdd.add(m); - } else { - threadPlaceHolder.threadCount = m.threadCount; - } - } else { - messagesToAdd.add(m); - } - } else { - messagesToAdd.add(m); - } - } else { - if (mQueryString != null) { - if (verifyAgainstSearch) { - messagesToSearch.add(message); - } else { - m = new MessageInfoHolder(); - FolderInfoHolder folderInfoHolder = new FolderInfoHolder( - mContext, 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(mContext, - 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; - } - - private MessageInfoHolder getThread(long threadId) { - for (MessageInfoHolder holder : mMessages) { - LocalMessage localMessage = (LocalMessage) holder.message; - if (localMessage.getId() == threadId || localMessage.getRootId() == threadId) { - 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(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(); - } - } - } - - - @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.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); - - 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; - } - - - 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; - - if (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(); - } - - int threadCount = message.threadCount; - if (mThreadId == -1 && threadCount > 1) { - holder.threadCount.setText(Integer.toString(threadCount)); - holder.threadCount.setVisibility(View.VISIBLE); - } else { - holder.threadCount.setVisibility(View.GONE); - } - - - // 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 ""; @@ -2200,31 +1652,212 @@ 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); + + view.setTag(holder); + + return view; + } + + @Override + public void bindView(View view, Context context, Cursor cursor) { + //TODO: make this work for search results + Account account = mAccount; + + 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 = 0; //TODO: get thread count from cursor + + 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; + + int adapterPosition = cursor.getPosition(); + boolean selected = mSelected.get(adapterPosition, false); + + if (selected) { + holder.chip.setBackgroundDrawable(account.getCheckmarkChip().drawable()); + } else { + holder.chip.setBackgroundDrawable(account.generateColorChip(read, toMe, ccMe, + fromMe, flagged).drawable()); + } + + // 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 (mThreadId == -1 && 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 { public TextView subject; public TextView preview; public TextView from; public TextView time; public TextView date; public View chip; - public int position = -1; public TextView threadCount; - - @Override - public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (position != -1) { - MessageInfoHolder message = (MessageInfoHolder) mAdapter.getItem(position); - toggleMessageSelect(message); - - - } - } } @@ -2286,23 +1919,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. * @@ -2311,40 +1927,54 @@ 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); + for (int i = 0, end = mSelectedCount; i < end; i++) { + mSelected.put(i, true); + } + + 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; + } + + boolean selected = mSelected.get(adapterPosition, false); + mSelected.put(adapterPosition, !selected); + if (mActionMode != null) { - if (mSelectedCount == 1 && holder.selected) { + if (mSelectedCount == 1 && selected) { mActionMode.finish(); + mActionMode = null; return; } } else { mActionMode = getSherlockActivity().startActionMode(mActionModeCallback); } - if (holder.selected) { - holder.selected = false; + if (selected) { mSelectedCount -= 1; } else { - holder.selected = true; mSelectedCount += 1; } - mAdapter.notifyDataSetChanged(); computeBatchDirection(); updateActionModeTitle(); @@ -2353,6 +1983,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mActionMode.invalidate(); computeSelectAllVisibility(); + + mAdapter.notifyDataSetChanged(); } private void updateActionModeTitle() { @@ -2367,6 +1999,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick boolean isBatchFlag = false; boolean isBatchRead = false; + /* for (MessageInfoHolder holder : mAdapter.getMessages()) { if (holder.selected) { if (!holder.flagged) { @@ -2381,54 +2014,48 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } } + */ + //TODO: implement mActionModeCallback.showMarkAsRead(isBatchRead); 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(new Message[] { message }, flag, newState); + } + + private void setFlag(Message[] messages, final Flag flag, final boolean newState) { + if (messages.length == 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; - } - } - mController.setFlag(messageList, flag, newState); - mAdapter.sortMessages(); + + mController.setFlag(messages, flag, newState); computeBatchDirection(); } + private void onMove(Message message) { + onMove(new Message[] { message }); + } + /** * Display the message move activity. * * @param holders * Never {@code null}. */ - private void onMove(final List holders) { - if (!checkCopyOrMovePossible(holders, FolderOperation.MOVE)) { + private void onMove(Message[] 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.length == 1 ? messages[0].getFolder() : mCurrentFolder.folder; + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_MOVE, folder, messages); + } + + private void onCopy(Message message) { + onCopy(new Message[] { message }); } /** @@ -2437,13 +2064,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param holders * Never {@code null}. */ - private void onCopy(final List holders) { - if (!checkCopyOrMovePossible(holders, FolderOperation.COPY)) { + private void onCopy(Message[] 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.length == 1 ? messages[0].getFolder() : mCurrentFolder.folder; + displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, messages); } /** @@ -2462,41 +2089,45 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * {@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 Message[] 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(); + private void onArchive(final Message message) { + onArchive(new Message[] { message }); + } + + private void onArchive(final Message[] messages) { + final String folderName = messages[0].getFolder().getAccount().getArchiveFolderName(); if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { return; } // TODO one should separate messages by account and call move afterwards // (because each account might have a specific Archive folder name) - move(holders, folderName); + move(messages, folderName); + } + + private void onSpam(Message message) { + onSpam(new Message[] { message }); } /** * @param holders * Never {@code null}. */ - private void onSpam(final List holders) { + private void onSpam(Message[] 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); } } @@ -2504,14 +2135,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param holders * Never {@code null}. */ - private void onSpamConfirmed(final List holders) { - final String folderName = holders.get(0).message.getFolder().getAccount().getSpamFolderName(); + private void onSpamConfirmed(Message[] messages) { + final String folderName = messages[0].getFolder().getAccount().getSpamFolderName(); if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) { return; } // TODO one should separate messages by account and call move afterwards // (because each account might have a specific Spam folder name) - move(holders, folderName); + move(messages, folderName); } private static enum FolderOperation { @@ -2528,23 +2159,25 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * * @return true if operation is possible */ - private boolean checkCopyOrMovePossible(final List holders, final FolderOperation operation) { - if (holders.isEmpty()) { + private boolean checkCopyOrMovePossible(final Message[] messages, final FolderOperation operation) { + if (messages.length == 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(); @@ -2554,42 +2187,14 @@ 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}. */ - private void copy(final List holders, final String destination) { - copyOrMove(holders, destination, FolderOperation.COPY); + private void copy(Message[] messages, final String destination) { + copyOrMove(messages, destination, FolderOperation.COPY); } /** @@ -2598,8 +2203,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param holders Never {@code null}. * @param destination Never {@code null}. */ - private void move(final List holders, final String destination) { - copyOrMove(holders, destination, FolderOperation.MOVE); + private void move(Message[] messages, final String destination) { + copyOrMove(messages, destination, FolderOperation.MOVE); } /** @@ -2616,7 +2221,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param operation * Never {@code null}. */ - private void copyOrMove(final List holders, final String destination, final FolderOperation operation) { + private void copyOrMove(Message[] messages, final String destination, final FolderOperation operation) { if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) { return; } @@ -2625,10 +2230,9 @@ 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(); @@ -2652,15 +2256,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick // 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, + mController.moveMessages(account, folderName, outMessages.toArray(new Message[outMessages.size()]), destination, null); - mAdapter.removeMessages(holders); } else { - mController.copyMessages(account, folderName, messages.toArray(new Message[messages.size()]), destination, + mController.copyMessages(account, folderName, outMessages.toArray(new Message[outMessages.size()]), destination, null); } } @@ -2715,11 +2318,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * TODO think of a better way then looping over all * messages. */ - final List selection = getSelectionFromCheckboxes(); + Message[] messages = getCheckedMessages(); Account account; - for (MessageInfoHolder holder : selection) { - account = holder.message.getFolder().getAccount(); + for (Message message : messages) { + account = message.getFolder().getAccount(); setContextCapabilities(account, menu); } @@ -2735,7 +2338,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick mMarkAsUnread = null; mFlag = null; mUnflag = null; - setAllSelected(false); + setSelectionState(false); } @Override @@ -2816,7 +2419,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - final List selection = getSelectionFromCheckboxes(); + Message[] messages = getCheckedMessages(); /* * In the following we assume that we can't move or copy @@ -2827,49 +2430,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; } @@ -2911,8 +2516,8 @@ 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); } /** @@ -2937,7 +2542,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick final Folder remoteFolder = remoteStore.getFolder(mSearchFolder); remoteFolder.close(); // Send a remoteSearchFinished() message for good measure. - mAdapter.mListener.remoteSearchFinished(searchAccount, mSearchFolder, 0, null); + //mAdapter.mListener.remoteSearchFinished(searchAccount, mSearchFolder, 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); @@ -2949,10 +2554,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick public ArrayList getMessageReferences() { ArrayList messageRefs = new ArrayList(); + /* for (MessageInfoHolder holder : mAdapter.getMessages()) { MessageReference ref = holder.message.makeMessageReference(); messageRefs.add(ref); } + */ + //TODO: implement return messageRefs; } @@ -3003,56 +2611,89 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick 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); + + //TODO: get account and folder from cursor + Folder folder = mCurrentFolder.folder; + + try { + return folder.getMessage(uid); + } catch (MessagingException e) { + Log.e(K9.LOG_TAG, "Something went wrong while fetching a message", e); + } + + return null; + } + + private Message[] getCheckedMessages() { + Message[] messages = new Message[mSelectedCount]; + int out = 0; + for (int position = 0, end = mAdapter.getCount(); position < end; position++) { + if (mSelected.get(position, false)) { + messages[out++] = getMessageAtPosition(position); + } + } + + return messages; } public void onDelete() { - MessageInfoHolder message = getSelection(); + Message message = getSelectedMessage(); if (message != null) { - onDelete(Collections.singletonList(message)); + onDelete(new Message[] { 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); + System.out.println("FLAGGED: " + message.isSet(Flag.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)); } } @@ -3124,4 +2765,39 @@ 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 = mAccount.getUuid(); + + Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages"); + + long folderId = 0; + try { + LocalFolder folder = (LocalFolder) mCurrentFolder.folder; + folder.open(OpenMode.READ_ONLY); + folderId = folder.getId(); + } catch (MessagingException e) { + //FIXME + e.printStackTrace(); + } + + String selection = MessageColumns.FOLDER_ID + "=?"; + String[] selectionArgs = { Long.toString(folderId) }; + + return new CursorLoader(getActivity(), uri, PROJECTION, selection, selectionArgs, + MessageColumns.DATE + " DESC"); + } + + @Override + public void onLoadFinished(Loader loader, Cursor data) { + mSelected = new SparseBooleanArray(data.getCount()); + mAdapter.swapCursor(data); + } + + @Override + public void onLoaderReset(Loader loader) { + mSelected = null; + mAdapter.swapCursor(null); + } } diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java index ae708f224..ce2e9d2ed 100644 --- a/src/com/fsck/k9/helper/MessageHelper.java +++ b/src/com/fsck/k9/helper/MessageHelper.java @@ -108,4 +108,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/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 8c42b6f48..641466b3c 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -3976,4 +3976,7 @@ public class LocalStore extends Store implements Serializable { } } + public LockableDatabase getDatabase() { + return database; + } } 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/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java new file mode 100644 index 000000000..1cf799d75 --- /dev/null +++ b/src/com/fsck/k9/provider/EmailProvider.java @@ -0,0 +1,246 @@ +package com.fsck.k9.provider; + +import java.util.List; + +import com.fsck.k9.Account; +import com.fsck.k9.Preferences; +import com.fsck.k9.helper.StringUtils; +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 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: + * - modify MessagingController (or LocalStore?) to call ContentResolver.notifyChange() to trigger + * notifications when the underlying data changes. + * - add support for message threading + * - add support for search views + * - add support for querying multiple accounts (e.g. "Unified Inbox") + * - 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"; + + + 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 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"; + } + + 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: { + List segments = uri.getPathSegments(); + String accountUuid = segments.get(1); + + cursor = getMessages(accountUuid, projection, selection, selectionArgs, sortOrder); + + cursor.setNotificationUri(contentResolver, uri); + break; + } + } + + return new IdTrickeryCursor(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 + "!=1"; + } else { + where = "(" + selection + ") AND " + + InternalMessageColumns.DELETED + "=0 AND " + + InternalMessageColumns.EMPTY + "!=1"; + } + + return db.query(MESSAGES_TABLE, projection, where, selectionArgs, null, null, + sortOrder); + } + }); + } 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); + } + } +} From f1e433e6df85d3978062092d7dfa4b618de909df Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 16 Oct 2012 22:42:51 +0200 Subject: [PATCH 18/74] Whitespace cleanup --- src/com/fsck/k9/activity/Accounts.java | 36 +- src/com/fsck/k9/activity/FolderList.java | 40 +- src/com/fsck/k9/activity/MessageList.java | 98 +-- .../k9/controller/MessagingController.java | 36 +- src/com/fsck/k9/mail/store/LocalStore.java | 60 +- .../k9/provider/UnreadWidgetProvider.java | 6 +- .../fsck/k9/search/ConditionsTreeNode.java | 742 +++++++++--------- src/com/fsck/k9/search/LocalSearch.java | 568 +++++++------- src/com/fsck/k9/search/SearchAccount.java | 58 +- .../fsck/k9/search/SearchSpecification.java | 216 ++--- 10 files changed, 930 insertions(+), 930 deletions(-) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index 596801554..9d6482a64 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -426,18 +426,18 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { * Creates and initializes the special accounts ('Unified Inbox' and 'All Messages') */ private void createSpecialAccounts() { - // create the unified inbox meta account ( all accounts is default when none specified ) + // create the unified inbox meta account ( all accounts is default when none specified ) String name = getString(R.string.integrated_inbox_title); LocalSearch tmpSearch = new LocalSearch(name); tmpSearch.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME); integratedInboxAccount = new SearchAccount(tmpSearch, name, - getString(R.string.integrated_inbox_detail)); - + getString(R.string.integrated_inbox_detail)); + // create the all messages search ( all accounts is default when none specified ) name = getString(R.string.search_all_messages_title); tmpSearch = new LocalSearch(name); - unreadAccount = new SearchAccount(tmpSearch, name, - getString(R.string.search_all_messages_detail)); + unreadAccount = new SearchAccount(tmpSearch, name, + getString(R.string.search_all_messages_detail)); } @SuppressWarnings("unchecked") @@ -563,7 +563,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { final SearchAccount searchAccount = (SearchAccount)account; MessagingController.getInstance(getApplication()) - .searchLocalMessages(searchAccount.getRelatedSearch(), new MessagingListener() { + .searchLocalMessages(searchAccount.getRelatedSearch(), new MessagingListener() { @Override public void searchStats(AccountStats stats) { mListener.accountStatusChanged(searchAccount, stats); @@ -637,9 +637,9 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { if (K9.FOLDER_NONE.equals(realAccount.getAutoExpandFolderName())) { FolderList.actionHandleAccount(this, realAccount); } else { - LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName()); - search.addAllowedFolder(realAccount.getAutoExpandFolderName()); - search.addAccountUuid(realAccount.getUuid()); + LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName()); + search.addAllowedFolder(realAccount.getAutoExpandFolderName()); + search.addAccountUuid(realAccount.getUuid()); MessageList.actionDisplaySearch(this, search, true);} } return true; @@ -1786,18 +1786,18 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { public void onClick(View v) { final String description = getString(R.string.search_title, account.getDescription(), getString(searchModifier.resId)); LocalSearch search = null; - + if (account instanceof SearchAccount) { - search = ((SearchAccount) account).getRelatedSearch(); - search.setName(description); + search = ((SearchAccount) account).getRelatedSearch(); + search.setName(description); } else { - search = new LocalSearch(description); - search.addAccountUuid(account.getUuid()); + search = new LocalSearch(description); + search.addAccountUuid(account.getUuid()); } - - search.allRequiredFlags(searchModifier.requiredFlags); - search.allForbiddenFlags(searchModifier.forbiddenFlags); - MessageList.actionDisplaySearch(Accounts.this, search, false); + + search.allRequiredFlags(searchModifier.requiredFlags); + search.allForbiddenFlags(searchModifier.forbiddenFlags); + MessageList.actionDisplaySearch(Accounts.this, search, false); } } diff --git a/src/com/fsck/k9/activity/FolderList.java b/src/com/fsck/k9/activity/FolderList.java index 6c8477833..487e8f44e 100644 --- a/src/com/fsck/k9/activity/FolderList.java +++ b/src/com/fsck/k9/activity/FolderList.java @@ -622,9 +622,9 @@ public class FolderList extends K9ListActivity implements OnNavigationListener { } private void onOpenFolder(String folder) { - LocalSearch search = new LocalSearch(folder); - search.addAccountUuid(mAccount.getUuid()); - search.addAllowedFolder(folder); + LocalSearch search = new LocalSearch(folder); + search.addAccountUuid(mAccount.getUuid()); + search.addAllowedFolder(folder); MessageList.actionDisplaySearch(this, search, false); } @@ -1265,18 +1265,18 @@ public class FolderList extends K9ListActivity implements OnNavigationListener { final String description = getString(R.string.search_title, getString(R.string.message_list_title, account.getDescription(), displayName), getString(searchModifier.resId)); - - 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, false); + + 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, false); } } @@ -1285,11 +1285,11 @@ public class FolderList extends K9ListActivity implements OnNavigationListener { 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(); - } + search.allRequiredFlags(new Flag[]{Flag.SEEN}); + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } } } diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index aca636bbd..9eae7ffbe 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -48,8 +48,8 @@ import com.fsck.k9.search.SearchSpecification.SearchCondition; */ public class MessageList extends K9FragmentActivity implements MessageListFragmentListener, OnBackStackChangedListener, OnSwipeGestureListener { - - // for this activity + + // for this activity private static final String EXTRA_SEARCH = "search"; // used for remote search @@ -59,22 +59,22 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme public static void actionDisplaySearch(Context context, SearchSpecification search, boolean newTask) { actionDisplaySearch(context, search, newTask, true); } - + public static void actionDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) { context.startActivity(intentDisplaySearch(context, search, newTask, clearTop)); } - + public static Intent intentDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) { Intent intent = new Intent(context, MessageList.class); intent.putExtra(EXTRA_SEARCH, search); - + if (clearTop) { - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); } if (newTask) { - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); } - + return intent; } @@ -91,11 +91,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme private Account mAccount; private String mFolderName; - private LocalSearch mSearch; + private LocalSearch mSearch; private boolean mSingleFolderMode; private boolean mSingleAccountMode; private boolean mIsRemote; - + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -123,8 +123,8 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme } private void decodeExtras(Intent intent) { - // check if this intent comes from the system search ( remote ) - if (intent.getStringExtra(SearchManager.QUERY) != 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 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA); @@ -132,40 +132,40 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme mSearch = new LocalSearch(); mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT)); mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER)); - + String query = intent.getStringExtra(SearchManager.QUERY); mSearch.or(new SearchCondition(SEARCHFIELD.SENDER, ATTRIBUTE.CONTAINS, query)); mSearch.or(new SearchCondition(SEARCHFIELD.SUBJECT, ATTRIBUTE.CONTAINS, query)); - + mIsRemote = true; } } } else { - // regular LocalSearch object was passed - mSearch = intent.getParcelableExtra(EXTRA_SEARCH); + // regular LocalSearch object was passed + mSearch = intent.getParcelableExtra(EXTRA_SEARCH); } - String[] accounts = mSearch.getAccountUuids(); - mSingleAccountMode = ( accounts != null && accounts.length == 1 - && !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS)); - mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1); - - if (mSingleAccountMode) { - mAccount = Preferences.getPreferences(this).getAccount(accounts[0]); - - 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); - } - - // now we know if we are in single account mode and need a subtitle - mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE); + String[] accounts = mSearch.getAccountUuids(); + mSingleAccountMode = ( accounts != null && accounts.length == 1 + && !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS)); + mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1); + + if (mSingleAccountMode) { + mAccount = Preferences.getPreferences(this).getAccount(accounts[0]); + + 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); + } + + // now we know if we are in single account mode and need a subtitle + mActionBarSubTitle.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE); } @Override @@ -329,10 +329,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme if (fragmentManager.getBackStackEntryCount() > 0) { fragmentManager.popBackStack(); } else if (!mSingleFolderMode) { - onBackPressed(); - } else { - onShowFolderList(); - } + onBackPressed(); + } else { + onShowFolderList(); + } return true; } case R.id.compose: { @@ -581,10 +581,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme @Override public void showMoreFromSameSender(String senderAddress) { - LocalSearch tmpSearch = new LocalSearch("From " + senderAddress); - tmpSearch.addAccountUuids(mSearch.getAccountUuids()); - tmpSearch.and(SEARCHFIELD.SENDER, senderAddress, ATTRIBUTE.CONTAINS); - + 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); @@ -668,10 +668,10 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme @Override public void showThread(Account account, String folderName, long threadRootId) { - LocalSearch tmpSearch = new LocalSearch(); - tmpSearch.addAccountUuids(mSearch.getAccountUuids()); - tmpSearch.and(SEARCHFIELD.THREAD_ROOT, String.valueOf(threadRootId), ATTRIBUTE.EQUALS); - + LocalSearch tmpSearch = new LocalSearch(); + tmpSearch.addAccountUuids(mSearch.getAccountUuids()); + tmpSearch.and(SEARCHFIELD.THREAD_ROOT, String.valueOf(threadRootId), ATTRIBUTE.EQUALS); + MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false); addMessageListFragment(fragment); } diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java index 181cb0177..8314866f3 100644 --- a/src/com/fsck/k9/controller/MessagingController.java +++ b/src/com/fsck/k9/controller/MessagingController.java @@ -632,20 +632,20 @@ public class MessagingController implements Runnable { } }); } - + public void searchLocalMessagesSynchronous(final LocalSearch search, final MessagingListener listener) { final AccountStats stats = new AccountStats(); 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; - } - + + // 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; + } + // Collecting statistics of the search result MessageRetrievalListener retrievalListener = new MessageRetrievalListener() { @Override @@ -666,15 +666,15 @@ public class MessagingController implements Runnable { } } }; - - // alert everyone the search has started + + // 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 localStore = account.getLocalStore(); localStore.searchForMessages(retrievalListener, search); } catch (Exception e) { if (listener != null) { @@ -686,9 +686,9 @@ public class MessagingController implements Runnable { listener.listLocalMessagesFinished(account, null); } } - } - - // publish the total search statistics + } + + // publish the total search statistics if (listener != null) { listener.searchStats(stats); } @@ -3164,7 +3164,7 @@ public class MessagingController implements Runnable { search.addAllowedFolder(account.getInboxFolderName()); search.addAccountUuid(account.getUuid()); Intent intent = MessageList.intentDisplaySearch(mApplication, search, true, true); - + PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0); builder.setContentIntent(pi); @@ -3250,7 +3250,7 @@ public class MessagingController implements Runnable { search.addAllowedFolder(account.getInboxFolderName()); search.addAccountUuid(account.getUuid()); Intent intent = MessageList.intentDisplaySearch(mApplication, search, true, true); - + PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0); builder.setContentIntent(pi); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index 13302b896..d4778bc3b 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -667,16 +667,16 @@ public class LocalStore extends Store implements Serializable { public Long doDbWork(final SQLiteDatabase db) { Cursor cursor = null; try { - cursor = db.rawQuery("SELECT id FROM folders WHERE name = '" + name + "'", null); + cursor = db.rawQuery("SELECT id FROM folders WHERE name = '" + name + "'", null); cursor.moveToFirst(); - return cursor.getLong(0); + return cursor.getLong(0); } finally { Utility.closeQuietly(cursor); } } }); } - + // TODO this takes about 260-300ms, seems slow. @Override public List getPersonalNamespaces(boolean forceListAll) throws MessagingException { @@ -920,7 +920,7 @@ public class LocalStore extends Store implements Serializable { } int i = 0; if (str.charAt(0) == '-') { - return false; + return false; } for (; i < length; i++) { char c = str.charAt(i); @@ -930,36 +930,36 @@ public class LocalStore extends Store implements Serializable { } return true; } - - public Message[] searchForMessages(MessageRetrievalListener retrievalListener, - LocalSearch search) throws MessagingException { - - // update some references in the search that have to be bound to this one store - for (ConditionsTreeNode node : search.getLeafSet()) { - if (node.mCondition.field == SEARCHFIELD.FOLDER) { - // TODO find better solution - if (isFolderId(node.mCondition.value)) { - continue; - } - - if (node.mCondition.value.equals(LocalSearch.GENERIC_INBOX_NAME)) { - node.mCondition.value = mAccount.getInboxFolderName(); - } - node.mCondition.value = String.valueOf(getFolderId(node.mCondition.value)); - } - } - // build sql query - String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE deleted = 0 " - + (search.getConditions() != null ? "AND (" + search.getConditions() + ")" : "") + " ORDER BY date DESC"; - + public Message[] searchForMessages(MessageRetrievalListener retrievalListener, + LocalSearch search) throws MessagingException { + + // update some references in the search that have to be bound to this one store + for (ConditionsTreeNode node : search.getLeafSet()) { + if (node.mCondition.field == SEARCHFIELD.FOLDER) { + // TODO find better solution + if (isFolderId(node.mCondition.value)) { + continue; + } + + if (node.mCondition.value.equals(LocalSearch.GENERIC_INBOX_NAME)) { + node.mCondition.value = mAccount.getInboxFolderName(); + } + node.mCondition.value = String.valueOf(getFolderId(node.mCondition.value)); + } + } + + // build sql query + String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE deleted = 0 " + + (search.getConditions() != null ? "AND (" + search.getConditions() + ")" : "") + " ORDER BY date DESC"; + if (K9.DEBUG) { Log.d(K9.LOG_TAG, "Query = " + sqlQuery); } - - return getMessages(retrievalListener, null, sqlQuery, new String[] {}); - } - + + return getMessages(retrievalListener, null, sqlQuery, new String[] {}); + } + /* * Given a query string, actually do the query for the messages and * call the MessageRetrievalListener for each one diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java index d8daeaa0b..180b8c22a 100644 --- a/src/com/fsck/k9/provider/UnreadWidgetProvider.java +++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java @@ -63,9 +63,9 @@ public class UnreadWidgetProvider extends AppWidgetProvider { clickIntent = FolderList.actionHandleAccountIntent(context, account, null, false); } else { - LocalSearch search = new LocalSearch(account.getAutoExpandFolderName()); - search.addAllowedFolder(account.getAutoExpandFolderName()); - search.addAccountUuid(account.getUuid()); + LocalSearch search = new LocalSearch(account.getAutoExpandFolderName()); + search.addAllowedFolder(account.getAutoExpandFolderName()); + search.addAccountUuid(account.getUuid()); clickIntent = MessageList.intentDisplaySearch(context, search, 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 index 650755a7d..03ba2039d 100644 --- a/src/com/fsck/k9/search/ConditionsTreeNode.java +++ b/src/com/fsck/k9/search/ConditionsTreeNode.java @@ -14,388 +14,388 @@ 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. + * 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 - * + * * @author dzan */ 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 + + 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; - } + 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; - } - - - /////////////////////////////////////////////////////////////// - // 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; - } - - - /** - * This will traverse the tree inorder and call toString recursively resulting - * in a valid SQL where clause. - */ - @Override - public String toString() { - return (mLeft == null ? "" : "(" + mLeft + ")") - + " " + ( mCondition == null ? mValue.name() : mCondition ) + " " - + (mRight == null ? "" : "(" + mRight + ")") ; - } - - /** - * 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; - } + /** + * 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; - /** - * 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 we ended up in a leaf, add ourself and return - if (mLeft == null && mRight == null) { - leafSet.add(this); - return leafSet; - // we didn't end up in a leaf - } else { - 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; - } + OPERATOR tmpValue = ConditionsTreeNode.OPERATOR.valueOf(cursor.getString(5)); - @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() { - public ConditionsTreeNode createFromParcel(Parcel in) { - return new ConditionsTreeNode(in); - } - - 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; - } - } + 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; + } + + + /////////////////////////////////////////////////////////////// + // 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; + } + + + /** + * This will traverse the tree inorder and call toString recursively resulting + * in a valid SQL where clause. + */ + @Override + public String toString() { + return (mLeft == null ? "" : "(" + mLeft + ")") + + " " + ( mCondition == null ? mValue.name() : mCondition ) + " " + + (mRight == null ? "" : "(" + mRight + ")") ; + } + + /** + * 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 we ended up in a leaf, add ourself and return + if (mLeft == null && mRight == null) { + leafSet.add(this); + return leafSet; + // we didn't end up in a leaf + } else { + 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() { + public ConditionsTreeNode createFromParcel(Parcel in) { + return new ConditionsTreeNode(in); + } + + 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 index c801a5308..28973482d 100644 --- a/src/com/fsck/k9/search/LocalSearch.java +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -11,81 +11,81 @@ import android.os.Parcelable; import com.fsck.k9.mail.Flag; /** - * This class represents a local search. + * 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. - * + * * @author dzan - * + * * 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; - - // 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 boolean mPredefined; + + // 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 - /////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////// + // Constructors + /////////////////////////////////////////////////////////////// /** - * Use this only if the search won't be saved. Saved searches need + * 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 + * 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 - } + 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 + } } - - - /////////////////////////////////////////////////////////////// - // Public manipulation methods - /////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////// + // Public manipulation methods + /////////////////////////////////////////////////////////////// /** * Sets the name of the saved search. If one existed it will * be overwritten. @@ -97,215 +97,215 @@ public class LocalSearch implements SearchSpecification { } /** - * Add a new account to the search. When no accounts are + * 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(); - } - mAccountUuids.add(uuid); + if (uuid.equals(ALL_ACCOUNTS)) { + mAccountUuids.clear(); + } + 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); - } - } - + 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); + 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; - } - - return mConditions.and(node); - } - - /** - * 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; - } - - return mConditions.or(node); - } - - /** - * 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 - */ - 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() { - for (ConditionsTreeNode node : getLeafSet()) { - if (node.getCondition().field == SEARCHFIELD.SUBJECT - || node.getCondition().field == SEARCHFIELD.SENDER ) { - return node.getCondition().value; - } - } - return null; - } - + + /** + * 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; + } + + return mConditions.and(node); + } + + /** + * 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; + } + + return mConditions.or(node); + } + + /** + * 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 + */ + 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() { + for (ConditionsTreeNode node : getLeafSet()) { + if (node.getCondition().field == SEARCHFIELD.SUBJECT + || node.getCondition().field == SEARCHFIELD.SENDER ) { + return node.getCondition().value; + } + } + return null; + } + /** * Returns the name of the saved search. * @@ -314,22 +314,22 @@ public class LocalSearch implements SearchSpecification { 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 isPredefined() { + return mPredefined; + } - /** - * Returns all the account uuids that this search will try to - * match against. - * - * @return Array of account uuids. - */ + /** + * 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) { @@ -346,43 +346,43 @@ public class LocalSearch implements SearchSpecification { * * @return The root node of the related conditions tree. */ - @Override - public ConditionsTreeNode getConditions() { - return mConditions; - } - - /////////////////////////////////////////////////////////////// - // Parcelable - /////////////////////////////////////////////////////////////// - @Override - public int describeContents() { - return 0; - } + @Override + public ConditionsTreeNode getConditions() { + return mConditions; + } - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(mName); - dest.writeByte((byte) (mPredefined ? 1 : 0)); - dest.writeStringList(new ArrayList(mAccountUuids)); - dest.writeParcelable(mConditions, flags); - } - - public static final Parcelable.Creator CREATOR - = new Parcelable.Creator() { - public LocalSearch createFromParcel(Parcel in) { - return new LocalSearch(in); - } - - public LocalSearch[] newArray(int size) { - return new LocalSearch[size]; - } - }; - - public LocalSearch(Parcel in) { - mName = in.readString(); - mPredefined = in.readByte() == 1; - mAccountUuids.addAll(in.createStringArrayList()); - mConditions = in.readParcelable(LocalSearch.class.getClassLoader()); - mLeafSet = mConditions.getLeafSet(); - } + /////////////////////////////////////////////////////////////// + // 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.writeStringList(new ArrayList(mAccountUuids)); + dest.writeParcelable(mConditions, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public LocalSearch createFromParcel(Parcel in) { + return new LocalSearch(in); + } + + public LocalSearch[] newArray(int size) { + return new LocalSearch[size]; + } + }; + + public LocalSearch(Parcel in) { + mName = in.readString(); + mPredefined = in.readByte() == 1; + mAccountUuids.addAll(in.createStringArrayList()); + mConditions = in.readParcelable(LocalSearch.class.getClassLoader()); + mLeafSet = 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 index d76a8a0dd..00df5c741 100644 --- a/src/com/fsck/k9/search/SearchAccount.java +++ b/src/com/fsck/k9/search/SearchAccount.java @@ -14,36 +14,36 @@ import com.fsck.k9.R; public class SearchAccount implements BaseAccount { // create the all messages search ( all accounts is default when none specified ) - public static SearchAccount createAllMessagesAccount(Context context) { + 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)); - } - + 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) { + + // 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.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME); return new SearchAccount(tmpSearch, name, - context.getString(R.string.integrated_inbox_detail)); - } - + context.getString(R.string.integrated_inbox_detail)); + } + private String mEmail = null; private String mDescription = null; private LocalSearch mSearch = null; private String mFakeUuid = null; - + public SearchAccount(LocalSearch search, String description, String email) throws IllegalArgumentException{ - if (search == null) { - throw new IllegalArgumentException("Provided LocalSearch was null"); - } - - this.mSearch = search; - this.mDescription = description; - this.mEmail = email; + if (search == null) { + throw new IllegalArgumentException("Provided LocalSearch was null"); + } + + this.mSearch = search; + this.mDescription = description; + this.mEmail = email; } @Override @@ -55,33 +55,33 @@ public class SearchAccount implements BaseAccount { 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; + return mSearch; } - + @Override /* * 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 + * 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 + * a Search then methods from LocalSearch will be called which do handle * things nice. */ public String getUuid() { - if (mFakeUuid == null){ - mFakeUuid = UUID.randomUUID().toString(); - } - return mFakeUuid; + if (mFakeUuid == null){ + mFakeUuid = UUID.randomUUID().toString(); + } + return mFakeUuid; } } diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java index 6f6a38fa5..4244b4b4c 100644 --- a/src/com/fsck/k9/search/SearchSpecification.java +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -4,79 +4,79 @@ 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. - */ + + /** + * 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 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"; - public static final String GENERIC_INBOX_NAME = "genericInboxName"; - - /////////////////////////////////////////////////////////////// - // ATTRIBUTE enum - /////////////////////////////////////////////////////////////// + public static final String GENERIC_INBOX_NAME = "genericInboxName"; + + /////////////////////////////////////////////////////////////// + // ATTRIBUTE enum + /////////////////////////////////////////////////////////////// public enum ATTRIBUTE { - CONTAINS(false), EQUALS(false), STARTSWITH(false), ENDSWITH(false), - NOT_CONTAINS(true), NOT_EQUALS(true), NOT_STARTSWITH(true), NOT_ENDSWITH(true); - - private boolean mNegation; - - private ATTRIBUTE(boolean negation) { - this.mNegation = negation; - } - + CONTAINS(false), EQUALS(false), STARTSWITH(false), ENDSWITH(false), + NOT_CONTAINS(true), NOT_EQUALS(true), NOT_STARTSWITH(true), NOT_ENDSWITH(true); + + private boolean mNegation; + + private ATTRIBUTE(boolean negation) { + this.mNegation = negation; + } + public String formQuery(String value) { - String queryPart = ""; - - switch (this) { - case NOT_CONTAINS: - case CONTAINS: - queryPart = "'%"+value+"%'"; - break; - case NOT_EQUALS: - case EQUALS: - queryPart = "'"+value+"'"; - break; - case NOT_STARTSWITH: - case STARTSWITH: - queryPart = "'%"+value+"'"; - break; - case NOT_ENDSWITH: - case ENDSWITH: - queryPart = "'"+value+"%'"; - break; - default: queryPart = "'"+value+"'"; - } - - return (mNegation ? " NOT LIKE " : " LIKE ") + queryPart; + String queryPart = ""; + + switch (this) { + case NOT_CONTAINS: + case CONTAINS: + queryPart = "'%"+value+"%'"; + break; + case NOT_EQUALS: + case EQUALS: + queryPart = "'"+value+"'"; + break; + case NOT_STARTSWITH: + case STARTSWITH: + queryPart = "'%"+value+"'"; + break; + case NOT_ENDSWITH: + case ENDSWITH: + queryPart = "'"+value+"%'"; + break; + default: queryPart = "'"+value+"'"; + } + + return (mNegation ? " NOT LIKE " : " LIKE ") + queryPart; } }; - - /////////////////////////////////////////////////////////////// - // SEARCHFIELD enum - /////////////////////////////////////////////////////////////// - /* - * Using an enum in order to have more robust code. Users ( & coders ) - * are prevented from passing illegal fields. No database overhead + + /////////////////////////////////////////////////////////////// + // 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. @@ -84,7 +84,7 @@ public interface SearchSpecification extends Parcelable { * 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("subject"), DATE("date"), UID("uid"), FLAG("flags"), @@ -93,7 +93,7 @@ public interface SearchSpecification extends Parcelable { ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"), THREAD_ROOT("thread_root"); private String dbName; - + private SEARCHFIELD(String dbName) { this.dbName = dbName; } @@ -102,25 +102,25 @@ public interface SearchSpecification extends Parcelable { return dbName; } } - - - /////////////////////////////////////////////////////////////// - // SearchCondition class - /////////////////////////////////////////////////////////////// + + + /////////////////////////////////////////////////////////////// + // SearchCondition class + /////////////////////////////////////////////////////////////// /** * This class represents 1 value for a certain search field. One - * value consists of three things: + * 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 String value; public ATTRIBUTE attribute; public SEARCHFIELD field; - + public SearchCondition(SEARCHFIELD field, ATTRIBUTE attribute, String value) { this.value = value; this.attribute = attribute; @@ -128,26 +128,26 @@ public interface SearchSpecification extends Parcelable { } private SearchCondition(Parcel in) { - this.value = in.readString(); - this.attribute = ATTRIBUTE.values()[in.readInt()]; - this.field = SEARCHFIELD.values()[in.readInt()]; + this.value = in.readString(); + this.attribute = ATTRIBUTE.values()[in.readInt()]; + this.field = SEARCHFIELD.values()[in.readInt()]; } - - public String toHumanString() { - return field.toString() + attribute.toString(); - } - - @Override - public String toString() { - return field.getDatabaseName() + attribute.formQuery(value); - } - + + public String toHumanString() { + return field.toString() + attribute.toString(); + } + + @Override + public String toString() { + return field.getDatabaseName() + attribute.formQuery(value); + } + @Override public boolean equals(Object o) { if (o instanceof SearchCondition) { SearchCondition tmp = (SearchCondition) o; if (tmp.attribute == attribute - && tmp.value.equals(value) + && tmp.value.equals(value) && tmp.field == field) { return true; } else { @@ -158,27 +158,27 @@ public interface SearchSpecification extends Parcelable { } } - @Override - public int describeContents() { - return 0; - } + @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() { - public SearchCondition createFromParcel(Parcel in) { - return new SearchCondition(in); - } - - public SearchCondition[] newArray(int size) { - return new SearchCondition[size]; - } - }; + @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() { + public SearchCondition createFromParcel(Parcel in) { + return new SearchCondition(in); + } + + public SearchCondition[] newArray(int size) { + return new SearchCondition[size]; + } + }; } } \ No newline at end of file From 502771dd0e0b8d382adc568b682394216bba9ea2 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 16 Oct 2012 22:48:31 +0200 Subject: [PATCH 19/74] Revert changing the default value for debug logging --- src/com/fsck/k9/K9.java | 4 ++-- src/com/fsck/k9/preferences/GlobalSettings.java | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/fsck/k9/K9.java b/src/com/fsck/k9/K9.java index ef0f21fb0..e6caa157e 100644 --- a/src/com/fsck/k9/K9.java +++ b/src/com/fsck/k9/K9.java @@ -579,8 +579,8 @@ public class K9 extends Application { public static void loadPrefs(Preferences prefs) { SharedPreferences sprefs = prefs.getPreferences(); - DEBUG = sprefs.getBoolean("enableDebugLogging", true); - DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", true); + DEBUG = sprefs.getBoolean("enableDebugLogging", false); + DEBUG_SENSITIVE = sprefs.getBoolean("enableSensitiveLogging", false); mAnimations = sprefs.getBoolean("animations", true); mGesturesEnabled = sprefs.getBoolean("gesturesEnabled", false); mUseVolumeKeysForNavigation = sprefs.getBoolean("useVolumeKeysForNavigation", false); diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java index 019391003..e49face93 100644 --- a/src/com/fsck/k9/preferences/GlobalSettings.java +++ b/src/com/fsck/k9/preferences/GlobalSettings.java @@ -64,10 +64,10 @@ public class GlobalSettings { new V(1, new DateFormatSetting(DateFormatter.DEFAULT_FORMAT)) )); s.put("enableDebugLogging", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(false)) )); s.put("enableSensitiveLogging", Settings.versions( - new V(1, new BooleanSetting(true)) + new V(1, new BooleanSetting(false)) )); s.put("fontSizeAccountDescription", Settings.versions( new V(1, new FontSizeSetting(FontSizes.SMALL)) From fff94956f4fda33468b65ad535da202e387ae545 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 16 Oct 2012 22:51:01 +0200 Subject: [PATCH 20/74] Fixed LocalSearch.addAllowedFolder() We need to use the node returned by add() as new root node. --- src/com/fsck/k9/search/LocalSearch.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java index 28973482d..72ec45578 100644 --- a/src/com/fsck/k9/search/LocalSearch.java +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -261,7 +261,7 @@ public class LocalSearch implements SearchSpecification { * - do and on root of it & rest of search * - do or between folder nodes */ - and(new SearchCondition(SEARCHFIELD.FOLDER, ATTRIBUTE.EQUALS, name)); + mConditions = and(new SearchCondition(SEARCHFIELD.FOLDER, ATTRIBUTE.EQUALS, name)); } /* From 20ed1ebe61e4bc933a0481b8c957728d3475ca44 Mon Sep 17 00:00:00 2001 From: cketti Date: Wed, 17 Oct 2012 20:52:03 +0200 Subject: [PATCH 21/74] Code style cleanup --- src/com/fsck/k9/activity/MessageList.java | 12 +-- src/com/fsck/k9/mail/store/LocalStore.java | 4 +- .../fsck/k9/search/ConditionsTreeNode.java | 89 +++++++++++-------- src/com/fsck/k9/search/LocalSearch.java | 36 ++++---- src/com/fsck/k9/search/SearchAccount.java | 22 ++--- src/com/fsck/k9/search/SearchModifier.java | 4 +- .../fsck/k9/search/SearchSpecification.java | 41 ++++----- 7 files changed, 113 insertions(+), 95 deletions(-) diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 9eae7ffbe..c5f148b8e 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -36,8 +36,8 @@ 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.Attribute; +import com.fsck.k9.search.SearchSpecification.Searchfield; import com.fsck.k9.search.SearchSpecification.SearchCondition; @@ -134,8 +134,8 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER)); String query = intent.getStringExtra(SearchManager.QUERY); - mSearch.or(new SearchCondition(SEARCHFIELD.SENDER, ATTRIBUTE.CONTAINS, query)); - mSearch.or(new SearchCondition(SEARCHFIELD.SUBJECT, ATTRIBUTE.CONTAINS, query)); + mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query)); + mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query)); mIsRemote = true; } @@ -583,7 +583,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme public void showMoreFromSameSender(String senderAddress) { LocalSearch tmpSearch = new LocalSearch("From " + senderAddress); tmpSearch.addAccountUuids(mSearch.getAccountUuids()); - tmpSearch.and(SEARCHFIELD.SENDER, senderAddress, ATTRIBUTE.CONTAINS); + tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS); MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false); @@ -670,7 +670,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme public void showThread(Account account, String folderName, long threadRootId) { LocalSearch tmpSearch = new LocalSearch(); tmpSearch.addAccountUuids(mSearch.getAccountUuids()); - tmpSearch.and(SEARCHFIELD.THREAD_ROOT, String.valueOf(threadRootId), ATTRIBUTE.EQUALS); + tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS); MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false); addMessageListFragment(fragment); diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index d4778bc3b..86b8b2a09 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -71,7 +71,7 @@ import com.fsck.k9.mail.store.StorageManager.StorageProvider; import com.fsck.k9.provider.AttachmentProvider; import com.fsck.k9.search.ConditionsTreeNode; import com.fsck.k9.search.LocalSearch; -import com.fsck.k9.search.SearchSpecification.SEARCHFIELD; +import com.fsck.k9.search.SearchSpecification.Searchfield; /** *
@@ -936,7 +936,7 @@ public class LocalStore extends Store implements Serializable {
 
         // update some references in the search that have to be bound to this one store
         for (ConditionsTreeNode node : search.getLeafSet()) {
-            if (node.mCondition.field == SEARCHFIELD.FOLDER) {
+            if (node.mCondition.field == Searchfield.FOLDER) {
                 // TODO find better solution
                 if (isFolderId(node.mCondition.value)) {
                     continue;
diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java
index 03ba2039d..430f31255 100644
--- a/src/com/fsck/k9/search/ConditionsTreeNode.java
+++ b/src/com/fsck/k9/search/ConditionsTreeNode.java
@@ -9,8 +9,8 @@ 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.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
 import com.fsck.k9.search.SearchSpecification.SearchCondition;
 
 /**
@@ -19,12 +19,10 @@ import com.fsck.k9.search.SearchSpecification.SearchCondition;
  *
  * TODO removing conditions from the tree
  * TODO implement NOT as a node again
- *
- * @author dzan
  */
-public class ConditionsTreeNode implements Parcelable{
+public class ConditionsTreeNode implements Parcelable {
 
-    public enum OPERATOR {
+    public enum Operator {
         AND, OR, CONDITION;
     }
 
@@ -36,7 +34,7 @@ public class ConditionsTreeNode implements Parcelable{
      * If mValue isn't CONDITION then mCondition contains a real
      * condition, otherwise it's null.
      */
-    public OPERATOR mValue;
+    public Operator mValue;
     public SearchCondition mCondition;
 
     /*
@@ -71,7 +69,7 @@ public class ConditionsTreeNode implements Parcelable{
         // other nodes
         while (cursor.moveToNext()) {
             tmp = buildNodeFromRow(cursor);
-            if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker ){
+            if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker) {
                 stack.peek().mLeft = tmp;
                 stack.push(tmp);
             } else {
@@ -94,11 +92,11 @@ public class ConditionsTreeNode implements Parcelable{
         ConditionsTreeNode result = null;
         SearchCondition condition = null;
 
-        OPERATOR tmpValue = ConditionsTreeNode.OPERATOR.valueOf(cursor.getString(5));
+        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));
+        if (tmpValue == Operator.CONDITION) {
+            condition = new SearchCondition(Searchfield.valueOf(cursor.getString(0)),
+                    Attribute.valueOf(cursor.getString(2)), cursor.getString(1));
         }
 
         result = new ConditionsTreeNode(condition);
@@ -116,10 +114,10 @@ public class ConditionsTreeNode implements Parcelable{
     public ConditionsTreeNode(SearchCondition condition) {
         mParent = null;
         mCondition = condition;
-        mValue = OPERATOR.CONDITION;
+        mValue = Operator.CONDITION;
     }
 
-    public ConditionsTreeNode(ConditionsTreeNode parent, OPERATOR op) {
+    public ConditionsTreeNode(ConditionsTreeNode parent, Operator op) {
         mParent = parent;
         mValue = op;
         mCondition = null;
@@ -138,7 +136,7 @@ public class ConditionsTreeNode implements Parcelable{
      * @throws Exception
      */
     public ConditionsTreeNode and(ConditionsTreeNode expr) throws Exception {
-        return add(expr, OPERATOR.AND);
+        return add(expr, Operator.AND);
     }
 
     /**
@@ -168,7 +166,7 @@ public class ConditionsTreeNode implements Parcelable{
      * @throws Exception
      */
     public ConditionsTreeNode or(ConditionsTreeNode expr) throws Exception {
-        return add(expr, OPERATOR.OR);
+        return add(expr, Operator.OR);
     }
 
     /**
@@ -244,10 +242,17 @@ public class ConditionsTreeNode implements Parcelable{
         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 );
+        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);
         }
 
@@ -273,7 +278,7 @@ public class ConditionsTreeNode implements Parcelable{
      * @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{
+    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.");
         }
@@ -285,8 +290,8 @@ public class ConditionsTreeNode implements Parcelable{
         if (mParent != null) {
             mParent.updateChild(this, tmpNode);
         }
-        this.mParent = tmpNode;
 
+        this.mParent = tmpNode;
         node.mParent = tmpNode;
 
         return tmpNode;
@@ -317,21 +322,21 @@ public class ConditionsTreeNode implements Parcelable{
      * @return Set of leaves being completed.
      */
     private HashSet getLeafSet(HashSet leafSet) {
-        // if we ended up in a leaf, add ourself and return
         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
-        } else {
-            if (mLeft != null) {
-                mLeft.getLeafSet(leafSet);
-            }
-
-            if (mRight != null) {
-                mRight.getLeafSet(leafSet);
-            }
-            return leafSet;
         }
+
+        // we didn't end up in a leaf
+        if (mLeft != null) {
+            mLeft.getLeafSet(leafSet);
+        }
+
+        if (mRight != null) {
+            mRight.getLeafSet(leafSet);
+        }
+        return leafSet;
     }
 
     /**
@@ -343,12 +348,15 @@ public class ConditionsTreeNode implements Parcelable{
      */
     private int applyMPTTLabel(int label) {
         mLeftMPTTMarker = label;
-        if (mLeft != null){
+
+        if (mLeft != null) {
             label = mLeft.applyMPTTLabel(label += 1);
         }
-        if (mRight != null){
+
+        if (mRight != null) {
             label = mRight.applyMPTTLabel(label += 1);
         }
+
         ++label;
         mRightMPTTMarker = label;
         return label;
@@ -374,26 +382,31 @@ public class ConditionsTreeNode implements Parcelable{
         dest.writeParcelable(mRight, flags);
     }
 
-    public static final Parcelable.Creator CREATOR
-        = new Parcelable.Creator() {
+    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()];
+        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
index 72ec45578..83c5fb522 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -12,12 +12,10 @@ 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.
  *
- * @author dzan
- *
  * TODO implement a complete addAllowedFolder method
  * TODO conflicting conditions check on add
  * TODO duplicate condition checking?
@@ -43,7 +41,7 @@ public class LocalSearch implements SearchSpecification {
      * Use this only if the search won't be saved. Saved searches need
      * a name!
      */
-    public LocalSearch(){}
+    public LocalSearch() {}
 
     /**
      *
@@ -141,7 +139,7 @@ public class LocalSearch implements SearchSpecification {
      *
      * @throws IllegalConditionException
      */
-    public void and(SEARCHFIELD field, String value, ATTRIBUTE attribute) {
+    public void and(Searchfield field, String value, Attribute attribute) {
         and(new SearchCondition(field, attribute, value));
     }
 
@@ -226,7 +224,7 @@ public class LocalSearch implements SearchSpecification {
     public void allRequiredFlags(Flag[] requiredFlags) {
         if (requiredFlags != null) {
             for (Flag f : requiredFlags) {
-                and(new SearchCondition(SEARCHFIELD.FLAG, ATTRIBUTE.CONTAINS, f.name()));
+                and(new SearchCondition(Searchfield.FLAG, Attribute.CONTAINS, f.name()));
             }
         }
     }
@@ -240,7 +238,7 @@ public class LocalSearch implements SearchSpecification {
     public void allForbiddenFlags(Flag[] forbiddenFlags) {
         if (forbiddenFlags != null) {
             for (Flag f : forbiddenFlags) {
-                and(new SearchCondition(SEARCHFIELD.FLAG, ATTRIBUTE.NOT_CONTAINS, f.name()));
+                and(new SearchCondition(Searchfield.FLAG, Attribute.NOT_CONTAINS, f.name()));
             }
         }
     }
@@ -261,7 +259,7 @@ public class LocalSearch implements SearchSpecification {
          *  		- do and on root of it & rest of search
          *  		- do or between folder nodes
          */
-        mConditions = and(new SearchCondition(SEARCHFIELD.FOLDER, ATTRIBUTE.EQUALS, name));
+        mConditions = and(new SearchCondition(Searchfield.FOLDER, Attribute.EQUALS, name));
     }
 
     /*
@@ -272,8 +270,8 @@ public class LocalSearch implements SearchSpecification {
     public List getFolderNames() {
         ArrayList results = new ArrayList();
         for (ConditionsTreeNode node : mLeafSet) {
-            if (node.mCondition.field == SEARCHFIELD.FOLDER
-                    && node.mCondition.attribute == ATTRIBUTE.EQUALS) {
+            if (node.mCondition.field == Searchfield.FOLDER &&
+                    node.mCondition.attribute == Attribute.EQUALS) {
                 results.add(node.mCondition.value);
             }
         }
@@ -298,8 +296,8 @@ public class LocalSearch implements SearchSpecification {
      */
     public String getRemoteSearchArguments() {
         for (ConditionsTreeNode node : getLeafSet()) {
-            if (node.getCondition().field == SEARCHFIELD.SUBJECT
-                    || node.getCondition().field == SEARCHFIELD.SENDER ) {
+            if (node.getCondition().field == Searchfield.SUBJECT ||
+                    node.getCondition().field == Searchfield.SENDER ) {
                 return node.getCondition().value;
             }
         }
@@ -311,8 +309,9 @@ public class LocalSearch implements SearchSpecification {
      *
      * @return Name of the search.
      */
+    @Override
     public String getName() {
-        return (mName == null ? "" : mName);
+        return (mName == null) ? "" : mName;
     }
 
     /**
@@ -333,7 +332,7 @@ public class LocalSearch implements SearchSpecification {
     @Override
     public String[] getAccountUuids() {
         if (mAccountUuids.size() == 0) {
-            return new String[] {SearchSpecification.ALL_ACCOUNTS};
+            return new String[] { SearchSpecification.ALL_ACCOUNTS };
         }
 
         String[] tmp = new String[mAccountUuids.size()];
@@ -367,12 +366,15 @@ public class LocalSearch implements SearchSpecification {
         dest.writeParcelable(mConditions, flags);
     }
 
-    public static final Parcelable.Creator CREATOR
-        = new Parcelable.Creator() {
+    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];
         }
@@ -380,7 +382,7 @@ public class LocalSearch implements SearchSpecification {
 
     public LocalSearch(Parcel in) {
         mName = in.readString();
-        mPredefined = in.readByte() == 1;
+        mPredefined = (in.readByte() == 1);
         mAccountUuids.addAll(in.createStringArrayList());
         mConditions = in.readParcelable(LocalSearch.class.getClassLoader());
         mLeafSet = mConditions.getLeafSet();
diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java
index 00df5c741..201b7eb0a 100644
--- a/src/com/fsck/k9/search/SearchAccount.java
+++ b/src/com/fsck/k9/search/SearchAccount.java
@@ -31,19 +31,21 @@ public class SearchAccount implements BaseAccount {
                 context.getString(R.string.integrated_inbox_detail));
     }
 
-    private String mEmail = null;
-    private String mDescription = null;
-    private LocalSearch mSearch = null;
-    private String mFakeUuid = null;
+    private String mEmail;
+    private String mDescription;
+    private LocalSearch mSearch;
+    private String mFakeUuid;
+
+    public SearchAccount(LocalSearch search, String description, String email)
+            throws IllegalArgumentException {
 
-    public SearchAccount(LocalSearch search, String description, String email) throws IllegalArgumentException{
         if (search == null) {
             throw new IllegalArgumentException("Provided LocalSearch was null");
         }
 
-        this.mSearch = search;
-        this.mDescription = description;
-        this.mEmail = email;
+        mSearch = search;
+        mDescription = description;
+        mEmail = email;
     }
 
     @Override
@@ -70,7 +72,6 @@ public class SearchAccount implements BaseAccount {
         return mSearch;
     }
 
-    @Override
     /*
      * 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
@@ -78,8 +79,9 @@ public class SearchAccount implements BaseAccount {
      * a Search then methods from LocalSearch will be called which do handle
      * things nice.
      */
+    @Override
     public String getUuid() {
-        if (mFakeUuid == null){
+        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
index f027ab062..22ec2740b 100644
--- a/src/com/fsck/k9/search/SearchModifier.java
+++ b/src/com/fsck/k9/search/SearchModifier.java
@@ -7,8 +7,8 @@ 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});
+    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;
diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
index 4244b4b4c..31bf9b387 100644
--- a/src/com/fsck/k9/search/SearchSpecification.java
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -34,13 +34,13 @@ public interface SearchSpecification extends Parcelable {
     ///////////////////////////////////////////////////////////////
     // ATTRIBUTE enum
     ///////////////////////////////////////////////////////////////
-    public enum ATTRIBUTE {
+    public enum Attribute {
         CONTAINS(false), EQUALS(false), STARTSWITH(false), ENDSWITH(false),
         NOT_CONTAINS(true), NOT_EQUALS(true), NOT_STARTSWITH(true), NOT_ENDSWITH(true);
 
         private boolean mNegation;
 
-        private ATTRIBUTE(boolean negation) {
+        private Attribute(boolean negation) {
             this.mNegation = negation;
         }
 
@@ -69,7 +69,7 @@ public interface SearchSpecification extends Parcelable {
 
             return (mNegation ? " NOT LIKE " : " LIKE ") + queryPart;
         }
-    };
+    }
 
     ///////////////////////////////////////////////////////////////
     // SEARCHFIELD enum
@@ -86,7 +86,7 @@ public interface SearchSpecification extends Parcelable {
      * 		preview, mime_type
      *
      */
-    public enum SEARCHFIELD {
+    public enum Searchfield {
         SUBJECT("subject"), DATE("date"), UID("uid"), FLAG("flags"),
         SENDER("sender_list"), TO("to_list"), CC("cc_list"), FOLDER("folder_id"),
         BCC("bcc_list"), REPLY_TO("reply_to_list"), MESSAGE("text_content"),
@@ -94,7 +94,7 @@ public interface SearchSpecification extends Parcelable {
 
         private String dbName;
 
-        private SEARCHFIELD(String dbName) {
+        private Searchfield(String dbName) {
             this.dbName = dbName;
         }
 
@@ -116,12 +116,12 @@ public interface SearchSpecification extends Parcelable {
      *
      * @author dzan
      */
-    public class SearchCondition implements Parcelable{
+    public class SearchCondition implements Parcelable {
         public String value;
-        public ATTRIBUTE attribute;
-        public SEARCHFIELD field;
+        public Attribute attribute;
+        public Searchfield field;
 
-        public SearchCondition(SEARCHFIELD field, ATTRIBUTE attribute, String value) {
+        public SearchCondition(Searchfield field, Attribute attribute, String value) {
             this.value = value;
             this.attribute = attribute;
             this.field = field;
@@ -129,8 +129,8 @@ public interface SearchSpecification extends Parcelable {
 
         private SearchCondition(Parcel in) {
             this.value = in.readString();
-            this.attribute = ATTRIBUTE.values()[in.readInt()];
-            this.field = SEARCHFIELD.values()[in.readInt()];
+            this.attribute = Attribute.values()[in.readInt()];
+            this.field = Searchfield.values()[in.readInt()];
         }
 
         public String toHumanString() {
@@ -146,16 +146,14 @@ public interface SearchSpecification extends Parcelable {
         public boolean equals(Object o) {
             if (o instanceof SearchCondition) {
                 SearchCondition tmp = (SearchCondition) o;
-                if (tmp.attribute == attribute
-                        && tmp.value.equals(value)
-                        && tmp.field == field) {
+                if (tmp.attribute == attribute &&
+                        tmp.field == field &&
+                        tmp.value.equals(value)) {
                     return true;
-                } else {
-                    return false;
                 }
-            } else {
-                return false;
             }
+
+            return false;
         }
 
         @Override
@@ -170,12 +168,15 @@ public interface SearchSpecification extends Parcelable {
             dest.writeInt(field.ordinal());
         }
 
-        public static final Parcelable.Creator CREATOR
-            = new Parcelable.Creator() {
+        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];
             }

From 1d655f5bc2ad129ec2ec36459da941969ff77acb Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 18 Oct 2012 05:15:40 +0200
Subject: [PATCH 22/74] Added ability to display special folders combining
 multiple accounts

---
 .../fsck/k9/fragment/MessageListFragment.java | 168 +++++++--
 src/com/fsck/k9/helper/MergeCursor.java       | 347 ++++++++++++++++++
 .../k9/helper/MergeCursorWithUniqueId.java    |  83 +++++
 src/com/fsck/k9/mail/store/LocalStore.java    |   4 +
 src/com/fsck/k9/provider/EmailProvider.java   | 187 +++++++++-
 5 files changed, 752 insertions(+), 37 deletions(-)
 create mode 100644 src/com/fsck/k9/helper/MergeCursor.java
 create mode 100644 src/com/fsck/k9/helper/MergeCursorWithUniqueId.java

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 31538910d..e3ba609a3 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -6,6 +6,8 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.Future;
@@ -24,6 +26,7 @@ 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;
@@ -68,8 +71,10 @@ 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.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;
@@ -77,18 +82,15 @@ 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;
 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.mail.store.LocalStore.LocalMessage;
 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.ConditionsTreeNode;
 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;
 import com.handmark.pulltorefresh.library.PullToRefreshBase;
 import com.handmark.pulltorefresh.library.PullToRefreshListView;
@@ -111,7 +113,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         MessageColumns.FOLDER_ID,
         MessageColumns.PREVIEW,
         MessageColumns.THREAD_ROOT,
-        MessageColumns.THREAD_PARENT
+        MessageColumns.THREAD_PARENT,
+        SpecialColumns.ACCOUNT_UUID
     };
 
     private static final int ID_COLUMN = 0;
@@ -128,6 +131,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private static final int PREVIEW_COLUMN = 11;
     private static final int THREAD_ROOT_COLUMN = 12;
     private static final int THREAD_PARENT_COLUMN = 13;
+    private static final int ACCOUNT_UUID_COLUMN = 14;
 
 
     public static MessageListFragment newInstance(LocalSearch search, boolean remoteSearch) {
@@ -321,8 +325,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private MessagingController mController;
 
     private Account mAccount;
+    private String[] mAccountUuids;
     private int mUnreadMessageCount = 0;
 
+    private Map mCursors = new HashMap();
+
     /**
      * Stores the name of the folder that we want to open as soon as possible
      * after load.
@@ -387,6 +394,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private final ActivityListener mListener = new MessageListActivityListener();
 
+    private Preferences mPreferences;
+
     /**
      * This class is used to run operations that modify UI elements in the UI thread.
      *
@@ -627,9 +636,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 //            long rootId = ((LocalMessage) message.message).getRootId();
 //            mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId);
         } else {
+            Account account = getAccountFromCursor(cursor);
+
+            long folderId = cursor.getLong(FOLDER_ID_COLUMN);
+            String folderName = getFolderNameById(account, folderId);
+
             MessageReference ref = new MessageReference();
-            ref.accountUuid = mAccount.getUuid();
-            ref.folderName = mCurrentFolder.name;
+            ref.accountUuid = account.getUuid();
+            ref.folderName = folderName;
             ref.uid = cursor.getString(UID_COLUMN);
             onOpenMessage(ref);
         }
@@ -653,6 +667,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();
@@ -686,7 +701,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         initializeMessageList();
 
-        getLoaderManager().initLoader(0, null, this);
+        LoaderManager loaderManager = getLoaderManager();
+        for (int i = 0, len = mAccountUuids.length; i < len; i++) {
+            loaderManager.initLoader(i, null, this);
+        }
     }
 
     private void decodeArguments() {
@@ -696,14 +714,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         mSearch = args.getParcelable(ARG_SEARCH);
         mTitle = args.getString(mSearch.getName());
 
-        Context appContext = getActivity().getApplicationContext();
-        String[] accounts = mSearch.getAccountUuids();
+        String[] accountUuids = mSearch.getAccountUuids();
 
         mSingleAccountMode = false;
-        if (accounts != null && accounts.length == 1
-                && !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
+        if (accountUuids.length == 1 && !accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) {
             mSingleAccountMode = true;
-            mAccount = Preferences.getPreferences(appContext).getAccount(accounts[0]);
+            mAccount = mPreferences.getAccount(accountUuids[0]);
         }
 
         mSingleFolderMode = false;
@@ -712,6 +728,23 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mFolderName = mSearch.getFolderNames().get(0);
             mCurrentFolder = getFolder(mFolderName, mAccount);
         }
+
+        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() {
@@ -744,6 +777,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
     }
 
+    private String getFolderNameById(Account account, long folderId) {
+        try {
+            LocalStore localStore = account.getLocalStore();
+            LocalFolder localFolder = localStore.getFolderById(folderId);
+            localFolder.open(OpenMode.READ_ONLY);
+            return localFolder.getName();
+        } catch (Exception e) {
+            Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e);
+            return null;
+        }
+    }
+
     @Override
     public void onPause() {
         super.onPause();
@@ -789,8 +834,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         mSenderAboveSubject = K9.messageListSenderAboveSubject();
 
-        final Preferences prefs = Preferences.getPreferences(appContext);
-
         // Check if we have connectivity.  Cache the value.
         if (mHasConnectivity == null) {
             final ConnectivityManager connectivityManager =
@@ -841,7 +884,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mSortAscending = account.isSortAscending(mSortType);
             mSortDateAscending = account.isSortAscending(SortType.SORT_DATE);
         } else {
-            accountsWithNotification = prefs.getAccounts();
+            accountsWithNotification = mPreferences.getAccounts();
             mSortType = K9.getSortType();
             mSortAscending = K9.isSortAscending(mSortType);
             mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE);
@@ -934,7 +977,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private void changeSort(SortType sortType, Boolean sortAscending) {
         mSortType = sortType;
 
-        Preferences prefs = Preferences.getPreferences(getActivity().getApplicationContext());
         Account account = mAccount;
 
         if (account != null) {
@@ -948,7 +990,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);
 
@@ -960,7 +1002,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();
         }
@@ -1244,8 +1286,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         getActivity().getMenuInflater().inflate(R.menu.message_list_item_context, menu);
 
-        //TODO: get account from cursor
-        Account account = mAccount;
+        Account account = getAccountFromCursor(cursor);
 
         String subject = cursor.getString(SUBJECT_COLUMN);
         String flagList = cursor.getString(FLAGS_COLUMN);
@@ -1511,8 +1552,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         @Override
         public void bindView(View view, Context context, Cursor cursor) {
-            //TODO: make this work for search results
-            Account account = mAccount;
+            Account account = getAccountFromCursor(cursor);
 
             String fromList = cursor.getString(SENDER_LIST_COLUMN);
             String toList = cursor.getString(TO_LIST_COLUMN);
@@ -2574,9 +2614,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             return false;
         }
 
-        Context appContext = getActivity().getApplicationContext();
-        final Preferences prefs = Preferences.getPreferences(appContext);
-
         boolean allowRemoteSearch = false;
         final Account searchAccount = mAccount;
         if (searchAccount != null) {
@@ -2593,13 +2630,61 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     @Override
     public Loader onCreateLoader(int id, Bundle args) {
-        String accountUuid = mAccount.getUuid();
+        String accountUuid = mAccountUuids[id];
+        Account account = mPreferences.getAccount(accountUuid);
 
         Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
 
+        StringBuilder query = new StringBuilder();
+        List queryArgs = new ArrayList();
+        buildQuery(account, mSearch.getConditions(), query, queryArgs);
+
+        String selection = query.toString();
+        String[] selectionArgs = queryArgs.toArray(new String[0]);
+
+        return new CursorLoader(getActivity(), uri, PROJECTION, selection, selectionArgs,
+                MessageColumns.DATE + " DESC");
+    }
+
+    private void buildQuery(Account account, ConditionsTreeNode node, StringBuilder query,
+            List selectionArgs) {
+
+        if (node.mLeft == null && node.mRight == null) {
+            SearchCondition condition = node.mCondition;
+            switch (condition.field) {
+                case FOLDER: {
+                    String folderName;
+                    //TODO: Fix the search condition used by the Unified Inbox
+                    if (LocalSearch.GENERIC_INBOX_NAME.equals(condition.value) ||
+                            "1".equals(condition.value)) {
+                        folderName = account.getInboxFolderName();
+                    } else {
+                        folderName = condition.value;
+                    }
+                    long folderId = getFolderId(account, folderName);
+                    query.append("folder_id = ?");
+                    selectionArgs.add(Long.toString(folderId));
+                    break;
+                }
+                default: {
+                    query.append(condition.toString());
+                }
+            }
+        } else {
+            query.append("(");
+            buildQuery(account, node.mLeft, query, selectionArgs);
+            query.append(") ");
+            query.append(node.mValue.name());
+            query.append(" (");
+            buildQuery(account, node.mRight, query, selectionArgs);
+            query.append(")");
+        }
+    }
+
+    private long getFolderId(Account account, String folderName) {
         long folderId = 0;
         try {
-            LocalFolder folder = (LocalFolder) mCurrentFolder.folder;
+            LocalFolder folder = (LocalFolder) getFolder(folderName, account).folder;
             folder.open(OpenMode.READ_ONLY);
             folderId = folder.getId();
         } catch (MessagingException e) {
@@ -2607,17 +2692,25 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             e.printStackTrace();
         }
 
-        String selection = MessageColumns.FOLDER_ID + "=?";
-        String[] selectionArgs = { Long.toString(folderId) };
-
-        return new CursorLoader(getActivity(), uri, PROJECTION, selection, selectionArgs,
-                MessageColumns.DATE + " DESC");
+        return folderId;
     }
 
     @Override
     public void onLoadFinished(Loader loader, Cursor data) {
-        mSelected = new SparseBooleanArray(data.getCount());
-        mAdapter.swapCursor(data);
+        mCursors.put(loader.getId(), data);
+
+        List list = new LinkedList(mCursors.keySet());
+        Collections.sort(list);
+        List cursors = new ArrayList(list.size());
+        for (Integer id : list) {
+            cursors.add(mCursors.get(id));
+        }
+
+        MergeCursorWithUniqueId cursor = new MergeCursorWithUniqueId(cursors);
+
+        mSelected = new SparseBooleanArray(cursor.getCount());
+        //TODO: use the (stable) IDs as index and reuse the old mSelected
+        mAdapter.swapCursor(cursor);
     }
 
     @Override
@@ -2625,4 +2718,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         mSelected = null;
         mAdapter.swapCursor(null);
     }
+
+    private Account getAccountFromCursor(Cursor cursor) {
+        String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
+        return mPreferences.getAccount(accountUuid);
+    }
 }
diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java
new file mode 100644
index 000000000..efba70d23
--- /dev/null
+++ b/src/com/fsck/k9/helper/MergeCursor.java
@@ -0,0 +1,347 @@
+package com.fsck.k9.helper;
+
+import java.util.List;
+
+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 List mCursors;
+
+    /**
+     * The currently active cursor.
+     */
+    protected Cursor mActiveCursor;
+
+    /**
+     * The index of the currently active cursor in {@link #mCursors}.
+     *
+     * @see #mActiveCursor
+     */
+    protected int mActiveCursorIndex;
+
+    /**
+     * Used to cache the value of {@link #getCount()}
+     */
+    private int mCount = -1;
+
+
+    /**
+     * Constructor
+     *
+     * @param cursors
+     *         The list of cursors this {@code MultiCursor} should combine.
+     */
+    public MergeCursor(List cursors) {
+        mCursors = cursors;
+        mActiveCursorIndex = 0;
+        mActiveCursor = cursors.get(0);
+    }
+
+    @Override
+    public void close() {
+        for (Cursor cursor : mCursors) {
+            cursor.close();
+        }
+    }
+
+    @Override
+    public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) {
+        mActiveCursor.copyStringToBuffer(columnIndex, buffer);
+    }
+
+    @Override
+    public void deactivate() {
+        for (Cursor cursor : mCursors) {
+            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) {
+                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() {
+        int pos = 0;
+        for (int i = 0; i < mActiveCursorIndex; i++) {
+            pos += mCursors.get(i).getCount();
+        }
+
+        return pos + mActiveCursor.getPosition();
+    }
+
+    @Override
+    public short getShort(int columnIndex) {
+        return mActiveCursor.getShort(columnIndex);
+    }
+
+    @Override
+    public String getString(int columnIndex) {
+        return mActiveCursor.getString(columnIndex);
+    }
+
+    @Override
+    public int getType(int columnIndex) {
+        return mActiveCursor.getType(columnIndex);
+    }
+
+    @Override
+    public boolean getWantsAllOnMoveCalls() {
+        return mActiveCursor.getWantsAllOnMoveCalls();
+    }
+
+    @Override
+    public boolean isAfterLast() {
+        if (mActiveCursorIndex == mCursors.size() - 1) {
+            return mActiveCursor.isAfterLast();
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isBeforeFirst() {
+        if (mActiveCursorIndex == 0) {
+            return mActiveCursor.isBeforeFirst();
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isClosed() {
+        return mActiveCursor.isClosed();
+    }
+
+    @Override
+    public boolean isFirst() {
+        if (mActiveCursorIndex == 0) {
+            return mActiveCursor.isFirst();
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isLast() {
+        if (mActiveCursorIndex == mCursors.size() - 1) {
+            return mActiveCursor.isLast();
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean isNull(int columnIndex) {
+        return mActiveCursor.isNull(columnIndex);
+    }
+
+    @Override
+    public boolean move(int offset) {
+        int ofs = offset;
+        int pos = mActiveCursor.getPosition();
+        if (offset >= 0) {
+            while (pos + ofs > mActiveCursor.getCount() &
+                    mActiveCursorIndex < mCursors.size() - 1) {
+
+                // Adjust the "move offset"
+                ofs -= mActiveCursor.getCount() - pos;
+
+                // Move to the next cursor
+                mActiveCursor = mCursors.get(++mActiveCursorIndex);
+
+                // Move the new cursor to the first position
+                mActiveCursor.moveToFirst();
+                pos = 0;
+            }
+        } else {
+            while (pos + ofs < 0 && mActiveCursorIndex > 0) {
+                // Adjust the "move offset"
+                ofs += pos;
+
+                // Move to the next cursor
+                mActiveCursor = mCursors.get(--mActiveCursorIndex);
+
+                // Move the new cursor to the first position
+                mActiveCursor.moveToLast();
+                pos = mActiveCursor.getPosition();
+            }
+        }
+
+        return mActiveCursor.move(ofs);
+    }
+
+    @Override
+    public boolean moveToFirst() {
+        mActiveCursorIndex = 0;
+        mActiveCursor = mCursors.get(mActiveCursorIndex);
+        return mActiveCursor.moveToFirst();
+    }
+
+    @Override
+    public boolean moveToLast() {
+        mActiveCursorIndex = mCursors.size() - 1;
+        mActiveCursor = mCursors.get(mActiveCursorIndex);
+        return mActiveCursor.moveToLast();
+    }
+
+    @Override
+    public boolean moveToNext() {
+        return move(1);
+    }
+
+    @Override
+    public boolean moveToPosition(int position) {
+        // Start at the beginning
+        mActiveCursorIndex = 0;
+        mActiveCursor = mCursors.get(mActiveCursorIndex);
+
+        int pos = position;
+        while (pos > mActiveCursor.getCount() - 1 &&
+                mActiveCursorIndex < mCursors.size() - 1) {
+
+            // Adjust the position
+            pos -= mActiveCursor.getCount();
+
+            // Move to the next cursor
+            mActiveCursor = mCursors.get(++mActiveCursorIndex);
+        }
+
+        return mActiveCursor.moveToPosition(pos);
+    }
+
+    @Override
+    public boolean moveToPrevious() {
+        return move(-1);
+    }
+
+    @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..284fcf9f4
--- /dev/null
+++ b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java
@@ -0,0 +1,83 @@
+package com.fsck.k9.helper;
+
+import java.util.List;
+
+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(List cursors) {
+        super(cursors);
+
+        if (cursors.size() > 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/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 86b8b2a09..8fe2577e1 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -661,6 +661,10 @@ 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
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index 1cf799d75..b1b774bfa 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -1,6 +1,9 @@
 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;
@@ -12,6 +15,7 @@ import com.fsck.k9.mail.store.LockableDatabase.DbCallback;
 import com.fsck.k9.mail.store.LockableDatabase.WrappedException;
 import com.fsck.k9.mail.store.UnavailableStorageException;
 
+import android.annotation.TargetApi;
 import android.content.ContentProvider;
 import android.content.ContentResolver;
 import android.content.ContentValues;
@@ -67,6 +71,9 @@ public class EmailProvider extends ContentProvider {
         //matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
     }
 
+    public interface SpecialColumns {
+        public static final String ACCOUNT_UUID = "account_uuid";
+    }
 
     public interface MessageColumns {
         public static final String ID = "id";
@@ -126,14 +133,31 @@ public class EmailProvider extends ContentProvider {
                 List segments = uri.getPathSegments();
                 String accountUuid = segments.get(1);
 
-                cursor = getMessages(accountUuid, projection, selection, selectionArgs, sortOrder);
+                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]);
+
+                cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs,
+                        sortOrder);
 
                 cursor.setNotificationUri(contentResolver, uri);
+
+                cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection,
+                        specialColumns);
+
                 break;
             }
         }
 
-        return new IdTrickeryCursor(cursor);
+        return cursor;
     }
 
     @Override
@@ -243,4 +267,163 @@ public class EmailProvider extends ContentProvider {
             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);
+        }
+    }
 }

From f093b84142dc3df06d352d8fc91701937522f44b Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sun, 21 Oct 2012 19:17:01 +0200
Subject: [PATCH 23/74] Make (de)serialization work when LocalSearch has no
 search conditions

---
 src/com/fsck/k9/search/LocalSearch.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 83c5fb522..0ffd3b488 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -385,6 +385,6 @@ public class LocalSearch implements SearchSpecification {
         mPredefined = (in.readByte() == 1);
         mAccountUuids.addAll(in.createStringArrayList());
         mConditions = in.readParcelable(LocalSearch.class.getClassLoader());
-        mLeafSet = mConditions.getLeafSet();
+        mLeafSet = (mConditions == null) ? null : mConditions.getLeafSet();
     }
 }
\ No newline at end of file

From 93ef3a7b0fba7f1f2fc569c5c11f40777f262553 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sun, 21 Oct 2012 19:18:57 +0200
Subject: [PATCH 24/74] Fix message list when no search condition is given (All
 messages)

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 4 ++++
 src/com/fsck/k9/search/LocalSearch.java           | 7 ++++++-
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index e3ba609a3..44b10de72 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -2649,6 +2649,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private void buildQuery(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) {
diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 0ffd3b488..1cca98b81 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -295,7 +295,12 @@ public class LocalSearch implements SearchSpecification {
      * very dirty fix for remotesearch support atm
      */
     public String getRemoteSearchArguments() {
-        for (ConditionsTreeNode node : getLeafSet()) {
+        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;

From 05a2571570a8ac639e361475049bc4a484f4bf79 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Mon, 22 Oct 2012 18:10:47 +0200
Subject: [PATCH 25/74] Fix display of named searches (e.g. "Unified Inbox")

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 44b10de72..ab58743bd 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -712,7 +712,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false);
         mSearch = args.getParcelable(ARG_SEARCH);
-        mTitle = args.getString(mSearch.getName());
+        mTitle = mSearch.getName();
 
         String[] accountUuids = mSearch.getAccountUuids();
 

From 95b39c71d2539061ef2c93669a228e1b70f8a1a7 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 23 Oct 2012 03:01:50 +0200
Subject: [PATCH 26/74] Add threading support to content provider

---
 src/com/fsck/k9/activity/MessageList.java     |  15 +--
 .../fsck/k9/fragment/MessageListFragment.java |  57 ++++++---
 src/com/fsck/k9/provider/EmailProvider.java   | 118 ++++++++++++++++--
 src/com/fsck/k9/search/LocalSearch.java       |   6 +-
 .../fsck/k9/search/SearchSpecification.java   |   3 +-
 5 files changed, 161 insertions(+), 38 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index c5f148b8e..020e07110 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -30,8 +30,6 @@ 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;
@@ -95,6 +93,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
     private boolean mSingleFolderMode;
     private boolean mSingleAccountMode;
     private boolean mIsRemote;
+    private boolean mThreadViewEnabled = true;  //TODO: this should be a setting
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -116,7 +115,8 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
         if (mMessageListFragment == null) {
             FragmentTransaction ft = fragmentManager.beginTransaction();
-            mMessageListFragment = MessageListFragment.newInstance(mSearch, mIsRemote);
+            mMessageListFragment = MessageListFragment.newInstance(mSearch, mThreadViewEnabled,
+                    mIsRemote);
             ft.add(R.id.message_list_container, mMessageListFragment);
             ft.commit();
         }
@@ -585,7 +585,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         tmpSearch.addAccountUuids(mSearch.getAccountUuids());
         tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS);
 
-        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
+        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false, false);
 
         addMessageListFragment(fragment);
     }
@@ -634,7 +634,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
     @Override
     public void remoteSearch(String searchAccount, String searchFolder, String queryString) {
-        MessageListFragment fragment = MessageListFragment.newInstance(mSearch, true);
+        MessageListFragment fragment = MessageListFragment.newInstance(mSearch, false, true);
 
         addMessageListFragment(fragment);
     }
@@ -669,10 +669,11 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
     @Override
     public void showThread(Account account, String folderName, long threadRootId) {
         LocalSearch tmpSearch = new LocalSearch();
-        tmpSearch.addAccountUuids(mSearch.getAccountUuids());
+        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);
+        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false, false);
         addMessageListFragment(fragment);
     }
 }
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index ab58743bd..21b9dbba8 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -2,6 +2,7 @@ package com.fsck.k9.fragment;
 
 import java.text.DateFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
@@ -99,7 +100,7 @@ import com.handmark.pulltorefresh.library.PullToRefreshListView;
 public class MessageListFragment extends SherlockFragment implements OnItemClickListener,
         ConfirmationDialogFragmentListener, LoaderCallbacks {
 
-    private static final String[] PROJECTION = {
+    private static final String[] THREADED_PROJECTION = {
         MessageColumns.ID,
         MessageColumns.UID,
         MessageColumns.INTERNAL_DATE,
@@ -114,7 +115,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         MessageColumns.PREVIEW,
         MessageColumns.THREAD_ROOT,
         MessageColumns.THREAD_PARENT,
-        SpecialColumns.ACCOUNT_UUID
+        SpecialColumns.ACCOUNT_UUID,
+
+        MessageColumns.THREAD_COUNT,
     };
 
     private static final int ID_COLUMN = 0;
@@ -132,12 +135,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private static final int THREAD_ROOT_COLUMN = 12;
     private static final int THREAD_PARENT_COLUMN = 13;
     private static final int ACCOUNT_UUID_COLUMN = 14;
+    private static final int THREAD_COUNT_COLUMN = 15;
+
+    private static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION,
+            THREAD_COUNT_COLUMN);
 
 
-    public static MessageListFragment newInstance(LocalSearch search, boolean remoteSearch) {
+    public static MessageListFragment newInstance(LocalSearch search, boolean threadedList,
+            boolean remoteSearch) {
         MessageListFragment fragment = new MessageListFragment();
         Bundle args = new Bundle();
         args.putParcelable(ARG_SEARCH, search);
+        args.putBoolean(ARG_THREADED_LIST, threadedList);
         args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
         fragment.setArguments(args);
         return fragment;
@@ -285,6 +294,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private static final int ACTIVITY_CHOOSE_FOLDER_COPY = 2;
 
     private static final String ARG_SEARCH = "searchObject";
+    private static final String ARG_THREADED_LIST = "threadedList";
     private static final String ARG_REMOTE_SEARCH = "remoteSearch";
     private static final String STATE_LIST_POSITION = "listPosition";
 
@@ -384,10 +394,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private DateFormat mTimeFormat;
 
-    //TODO: make this a setting
-    private boolean mThreadViewEnabled = true;
-
-    private long mThreadId;
+    private boolean mThreadedList;
 
 
     private Context mContext;
@@ -631,21 +638,22 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         Cursor cursor = (Cursor) parent.getItemAtPosition(position);
         if (mSelectedCount > 0) {
             toggleMessageSelect(position);
-//        } else if (message.threadCount > 1) {
-//            Folder folder = message.message.getFolder();
-//            long rootId = ((LocalMessage) message.message).getRootId();
-//            mFragmentListener.showThread(folder.getAccount(), folder.getName(), rootId);
         } else {
             Account account = getAccountFromCursor(cursor);
 
             long folderId = cursor.getLong(FOLDER_ID_COLUMN);
             String folderName = getFolderNameById(account, folderId);
 
-            MessageReference ref = new MessageReference();
-            ref.accountUuid = account.getUuid();
-            ref.folderName = folderName;
-            ref.uid = cursor.getString(UID_COLUMN);
-            onOpenMessage(ref);
+            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);
+            }
         }
     }
 
@@ -710,6 +718,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private void decodeArguments() {
         Bundle args = getArguments();
 
+        mThreadedList = args.getBoolean(ARG_THREADED_LIST, false);
         mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false);
         mSearch = args.getParcelable(ARG_SEARCH);
         mTitle = mSearch.getName();
@@ -1580,7 +1589,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 subject = getString(R.string.general_no_subject);
             }
 
-            int threadCount = 0;    //TODO: get thread count from cursor
+            int threadCount = (mThreadedList) ? cursor.getInt(THREAD_COUNT_COLUMN) : 0;
 
             String flagList = cursor.getString(FLAGS_COLUMN);
             String[] flags = flagList.split(",");
@@ -1641,7 +1650,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             }
 
             // Thread count
-            if (mThreadId == -1 && threadCount > 1) {
+            if (threadCount > 1) {
                 holder.threadCount.setText(Integer.toString(threadCount));
                 holder.threadCount.setVisibility(View.VISIBLE);
             } else {
@@ -2633,7 +2642,15 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         String accountUuid = mAccountUuids[id];
         Account account = mPreferences.getAccount(accountUuid);
 
-        Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
+        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();
@@ -2642,7 +2659,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         String selection = query.toString();
         String[] selectionArgs = queryArgs.toArray(new String[0]);
 
-        return new CursorLoader(getActivity(), uri, PROJECTION, selection, selectionArgs,
+        return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs,
                 MessageColumns.DATE + " DESC");
     }
 
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index b1b774bfa..55fff0e96 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -38,9 +38,6 @@ import android.net.Uri;
  * TODO:
  * - modify MessagingController (or LocalStore?) to call ContentResolver.notifyChange() to trigger
  *   notifications when the underlying data changes.
- * - add support for message threading
- * - add support for search views
- * - add support for querying multiple accounts (e.g. "Unified Inbox")
  * - add support for account list and folder list
  */
 public class EmailProvider extends ContentProvider {
@@ -56,18 +53,42 @@ public class EmailProvider extends ContentProvider {
      */
     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_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/*/messages/threaded", MESSAGES_THREADED);
         //matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
     }
 
@@ -93,6 +114,7 @@ public class EmailProvider extends ContentProvider {
         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 {
@@ -129,7 +151,8 @@ public class EmailProvider extends ContentProvider {
         ContentResolver contentResolver = getContext().getContentResolver();
         Cursor cursor = null;
         switch (match) {
-            case MESSAGES: {
+            case MESSAGES:
+            case MESSAGES_THREADED: {
                 List segments = uri.getPathSegments();
                 String accountUuid = segments.get(1);
 
@@ -145,8 +168,15 @@ public class EmailProvider extends ContentProvider {
 
                 String[] dbProjection = dbColumnNames.toArray(new String[0]);
 
-                cursor = getMessages(accountUuid, dbProjection, selection, selectionArgs,
-                        sortOrder);
+                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);
 
@@ -206,6 +236,78 @@ public class EmailProvider extends ContentProvider {
         }
     }
 
+    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 {
+                            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) " +
+                            "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(addPrefixToSelection(MESSAGES_COLUMNS, "h.", selection));
+                        query.append(") ");
+                    }
+
+                    query.append("GROUP BY h.id");
+
+                    if (!StringUtils.isNullOrEmpty(sortOrder)) {
+                        query.append(" ORDER BY ");
+                        query.append(sortOrder);
+                    }
+
+                    return db.rawQuery(query.toString(), selectionArgs);
+                }
+            });
+        } catch (UnavailableStorageException e) {
+            throw new RuntimeException("Storage not available", e);
+        }
+    }
+
+    private 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;
+    }
+
     private Account getAccount(String accountUuid) {
         if (mPreferences == null) {
             Context appContext = getContext().getApplicationContext();
diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 1cca98b81..c52dec469 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -176,7 +176,8 @@ public class LocalSearch implements SearchSpecification {
             return node;
         }
 
-        return mConditions.and(node);
+        mConditions = mConditions.and(node);
+        return mConditions;
     }
 
     /**
@@ -212,7 +213,8 @@ public class LocalSearch implements SearchSpecification {
             return node;
         }
 
-        return mConditions.or(node);
+        mConditions = mConditions.or(node);
+        return mConditions;
     }
 
     /**
diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
index 31bf9b387..8f22622c6 100644
--- a/src/com/fsck/k9/search/SearchSpecification.java
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -90,7 +90,8 @@ public interface SearchSpecification extends Parcelable {
         SUBJECT("subject"), DATE("date"), UID("uid"), FLAG("flags"),
         SENDER("sender_list"), TO("to_list"), CC("cc_list"), FOLDER("folder_id"),
         BCC("bcc_list"), REPLY_TO("reply_to_list"), MESSAGE("text_content"),
-        ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"), THREAD_ROOT("thread_root");
+        ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"), THREAD_ROOT("thread_root"),
+        ID("id");
 
         private String dbName;
 

From 1b98ce57e14bc67ffa1bdade1e7954a5576b2bc5 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 00:08:44 +0200
Subject: [PATCH 27/74] Notify observers (e.g. CursorAdapter) when the messages
 table changed

---
 src/com/fsck/k9/mail/store/LocalStore.java | 29 +++++++++++++++++++++-
 1 file changed, 28 insertions(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 8fe2577e1..c65e57ce4 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -26,6 +26,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;
@@ -69,6 +70,7 @@ 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.ConditionsTreeNode;
 import com.fsck.k9.search.LocalSearch;
 import com.fsck.k9.search.SearchSpecification.Searchfield;
@@ -112,6 +114,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)}
@@ -124,6 +128,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();
 
@@ -2191,6 +2196,9 @@ public class LocalStore extends Store implements Serializable {
                         return null;
                     }
                 });
+
+                notifyChange();
+
                 return uidMap;
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
@@ -2492,6 +2500,9 @@ public class LocalStore extends Store implements Serializable {
                         return null;
                     }
                 });
+
+                notifyChange();
+
                 return uidMap;
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
@@ -2572,6 +2583,8 @@ public class LocalStore extends Store implements Serializable {
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
             }
+
+            notifyChange();
         }
 
         /**
@@ -2821,6 +2834,9 @@ public class LocalStore extends Store implements Serializable {
                     return null;
                 }
             });
+
+            //TODO: remove this once the UI code exclusively uses the database id
+            notifyChange();
         }
 
         @Override
@@ -2865,6 +2881,8 @@ public class LocalStore extends Store implements Serializable {
                 }
             });
             resetUnreadAndFlaggedCounts();
+
+            notifyChange();
         }
 
         public void clearMessagesOlderThan(long cutoff) throws MessagingException {
@@ -3592,7 +3610,7 @@ public class LocalStore extends Store implements Serializable {
                 throw(MessagingException) e.getCause();
             }
 
-
+            notifyChange();
         }
 
         /*
@@ -3648,6 +3666,8 @@ public class LocalStore extends Store implements Serializable {
                 throw(MessagingException) e.getCause();
             }
             ((LocalFolder)mFolder).deleteHeaders(mId);
+
+            notifyChange();
         }
 
         /*
@@ -3768,6 +3788,8 @@ public class LocalStore extends Store implements Serializable {
             } catch (WrappedException e) {
                 throw(MessagingException) e.getCause();
             }
+
+            notifyChange();
         }
 
         private void updateFolderCountsOnFlag(Flag flag, boolean set) {
@@ -3962,4 +3984,9 @@ public class LocalStore extends Store implements Serializable {
     public LockableDatabase getDatabase() {
         return database;
     }
+
+    private void notifyChange() {
+        Uri uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + uUid + "/messages");
+        mContentResolver.notifyChange(uri, null);
+    }
 }

From cba9d8fc88acdc71b1a367edcc04e7b371911c18 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 00:09:33 +0200
Subject: [PATCH 28/74] Fix pull to refresh for normal list vs. "remote
 search"-able

---
 .../fsck/k9/fragment/MessageListFragment.java   | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 21b9dbba8..9657b9802 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -857,15 +857,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (mSingleFolderMode) {
-            if (!mAccount.allowRemoteSearch()) {
-                mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
-                    @Override
-                    public void onRefresh(PullToRefreshBase refreshView) {
-                        checkMail();
-                    }
-                });
-            // TODO this has to go! find better remote search integration
-            } else {
+            if (mRemoteSearch && mAccount.allowRemoteSearch()) {
                 mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
                     @Override
                     public void onRefresh(PullToRefreshBase refreshView) {
@@ -875,6 +867,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 });
                 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);

From 3bef05b55d670fcafdaaaa859ec7bd35feb0ef5e Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 00:32:29 +0200
Subject: [PATCH 29/74] Removed LocalStore hack introduced in 16d2326

---
 src/com/fsck/k9/mail/store/LocalStore.java | 22 ++++------------------
 1 file changed, 4 insertions(+), 18 deletions(-)

diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index c65e57ce4..fdb1c2af3 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -98,10 +98,6 @@ public class LocalStore extends Store implements Serializable {
         "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, thread_root, thread_parent, empty ";
 
-    static private String GET_MESSAGES_COLS_PREFIX =
-            "m.subject, m.sender_list, m.date, m.uid, m.flags, m.id, m.to_list, m.cc_list, " +
-            "m.bcc_list, m.reply_to_list, m.attachment_count, m.internal_date, m.message_id, " +
-            "m.folder_id, m.preview, m.thread_root, m.thread_parent, m.empty ";
 
     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";
 
@@ -1883,23 +1879,13 @@ public class LocalStore extends Store implements Serializable {
 
                             try {
                                 cursor = db.rawQuery(
-                                        "SELECT " +
-                                        GET_MESSAGES_COLS_PREFIX + ", COUNT(c.id) " +
-                                        "FROM messages m LEFT JOIN messages c " +
-                                        "ON (" +
-                                            "(" +
-                                                "m.thread_root IN (c.thread_root, c.id)" +
-                                                " OR " +
-                                                "(m.thread_root IS NULL AND m.id IN (c.thread_root, c.id))" +
-                                            ") AND c.empty != 1) " +
-                                        "WHERE m.uid = ? AND m.folder_id = ?",
+                                             "SELECT "
+                                             + GET_MESSAGES_COLS
+                                             + "FROM messages WHERE uid = ? AND folder_id = ?",
                                              new String[] {
                                                  message.getUid(), Long.toString(mFolderId)
                                              });
-
-                                // Note: Because of the COUNT(c.id) we will always get one result
-                                //       row even if nothing was found. So we check if 'id' is NULL
-                                if (!cursor.moveToNext() || cursor.isNull(0)) {
+                                if (!cursor.moveToNext()) {
                                     return null;
                                 }
                                 message.populateFromGetMessageCursor(cursor);

From 1fcce6fb8a22f2d538ea05d5b4e48d1c998e03c1 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 01:03:59 +0200
Subject: [PATCH 30/74] Add database indices for thread columns in 'messages'
 table

---
 src/com/fsck/k9/mail/store/LocalStore.java | 27 ++++++++++++++++++++--
 1 file changed, 25 insertions(+), 2 deletions(-)

diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index fdb1c2af3..4407415b9 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -102,7 +102,7 @@ public class LocalStore extends Store implements Serializable {
     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";
 
 
-    protected static final int DB_VERSION = 44;
+    protected static final int DB_VERSION = 45;
 
     protected String uUid = null;
 
@@ -204,7 +204,14 @@ public class LocalStore extends Store implements Serializable {
                         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)");
 
-                        //TODO: add indices for the thread columns
+                        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,"
@@ -403,6 +410,22 @@ public class LocalStore extends Store implements Serializable {
                                 }
                             }
                         }
+                        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");

From ec76dca57fdc862f171040b273f4afb19ae66ded Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 01:37:33 +0200
Subject: [PATCH 31/74] Remove unused stuff

---
 .../fsck/k9/activity/MessageInfoHolder.java   |   1 -
 .../k9/controller/MessagingController.java    | 110 ------------
 src/com/fsck/k9/helper/MessageHelper.java     |   3 -
 src/com/fsck/k9/mail/Message.java             |  11 --
 .../fsck/k9/mail/internet/MimeMessage.java    |  23 ---
 src/com/fsck/k9/mail/store/LocalStore.java    | 166 +-----------------
 6 files changed, 3 insertions(+), 311 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageInfoHolder.java b/src/com/fsck/k9/activity/MessageInfoHolder.java
index 56db97ced..4669daec6 100644
--- a/src/com/fsck/k9/activity/MessageInfoHolder.java
+++ b/src/com/fsck/k9/activity/MessageInfoHolder.java
@@ -24,7 +24,6 @@ public class MessageInfoHolder {
     public boolean selected;
     public String account;
     public String uri;
-    public int threadCount;
 
     // Empty constructor for comparison
     public MessageInfoHolder() {
diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 8314866f3..2c3ee24ea 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -510,116 +510,6 @@ public class MessagingController implements Runnable {
         });
     }
 
-
-
-    /**
-     * List the messages in the local message store for the given folder asynchronously.
-     *
-     * @param account
-     * @param folder
-     * @param listener
-     * @param threaded
-     * @param threadId
-     * @throws MessagingException
-     */
-    public void listLocalMessages(final Account account, final String folder,
-            final MessagingListener listener, final boolean threaded, final long threadId) {
-        threadPool.execute(new Runnable() {
-            @Override
-            public void run() {
-                listLocalMessagesSynchronous(account, folder, listener, threaded, threadId);
-            }
-        });
-    }
-
-
-    /**
-     * List the messages in the local message store for the given folder synchronously.
-     *
-     * @param account
-     * @param folder
-     * @param listener
-     * @param threaded
-     * @param threadId
-     * @throws MessagingException
-     */
-    public void listLocalMessagesSynchronous(final Account account, final String folder,
-            final MessagingListener listener, boolean threaded, long threadId) {
-
-        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);
-
-            if (threadId != -1) {
-                localFolder.getMessagesInThread(threadId, retrievalListener);
-            } else if (threaded) {
-                localFolder.getThreadedMessages(retrievalListener);
-            } else {
-                localFolder.getMessages(retrievalListener, false);
-            }
-
-            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);
-        }
-    }
-
     /**
      * Find all messages in any local account which match the query 'query'
      * @throws MessagingException
diff --git a/src/com/fsck/k9/helper/MessageHelper.java b/src/com/fsck/k9/helper/MessageHelper.java
index ce2e9d2ed..51ab08705 100644
--- a/src/com/fsck/k9/helper/MessageHelper.java
+++ b/src/com/fsck/k9/helper/MessageHelper.java
@@ -17,7 +17,6 @@ import com.fsck.k9.mail.Flag;
 import com.fsck.k9.mail.Message;
 import com.fsck.k9.mail.MessagingException;
 import com.fsck.k9.mail.Message.RecipientType;
-import com.fsck.k9.mail.store.LocalStore.LocalMessage;
 import com.fsck.k9.helper.DateFormatter;
 
 public class MessageHelper {
@@ -87,8 +86,6 @@ public class MessageHelper {
             target.account = account.getUuid();
             target.uri = "email://messages/" + account.getAccountNumber() + "/" + message.getFolder().getName() + "/" + message.getUid();
 
-            target.threadCount = ((LocalMessage) message).getThreadCount();
-
         } catch (MessagingException me) {
             Log.w(K9.LOG_TAG, "Unable to load message info", me);
         }
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/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 4407415b9..459015064 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -88,15 +88,13 @@ public class LocalStore extends Store implements Serializable {
     private static final String[] EMPTY_STRING_ARRAY = new String[0];
     private static final Flag[] EMPTY_FLAG_ARRAY = new Flag[0];
 
-    private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED, Flag.X_DESTROYED, Flag.SEEN, Flag.FLAGGED };
-
     /*
      * 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, thread_root, thread_parent, empty ";
+        + "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";
@@ -1960,76 +1958,6 @@ public class LocalStore extends Store implements Serializable {
             }
         }
 
-        public Message[] getThreadedMessages(final MessageRetrievalListener listener)
-                throws MessagingException {
-            try {
-                return database.execute(false, new DbCallback() {
-                    @Override
-                    public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException,
-                            UnavailableStorageException {
-                        try {
-                            open(OpenMode.READ_WRITE);
-
-                            return LocalStore.this.getMessages(
-                                    listener,
-                                    LocalFolder.this,
-                                    "SELECT " +
-                                    "m.subject, m.sender_list, MAX(m.date), m.uid, m.flags, " +
-                                    "m.id, m.to_list, m.cc_list, m.bcc_list, m.reply_to_list, " +
-                                    "m.attachment_count, m.internal_date, m.message_id, " +
-                                    "m.folder_id, m.preview, m.thread_root, m.thread_parent, " +
-                                    "m.empty, COUNT(h.id) " +
-                                    "FROM messages h JOIN messages m " +
-                                    "ON (h.id = m.thread_root OR h.id = m.id) " +
-                                    "WHERE h.deleted = 0 AND m.deleted = 0 AND " +
-                                    "(m.empty IS NULL OR m.empty != 1) AND " +
-                                    "h.thread_root IS NULL AND h.folder_id = ? " +
-                                    "GROUP BY h.id",
-                                    new String[] { Long.toString(mFolderId) });
-                        } catch (MessagingException e) {
-                            throw new WrappedException(e);
-                        }
-                    }
-                });
-            } catch (WrappedException e) {
-                throw(MessagingException) e.getCause();
-            }
-        }
-
-        public Message[] getMessagesInThread(final long threadId,
-                final MessageRetrievalListener listener) throws MessagingException {
-            try {
-                return database.execute(false, new DbCallback() {
-                    @Override
-                    public Message[] doDbWork(final SQLiteDatabase db) throws WrappedException,
-                            UnavailableStorageException {
-                        try {
-                            open(OpenMode.READ_WRITE);
-
-                            String threadIdString = Long.toString(threadId);
-
-                            return LocalStore.this.getMessages(
-                                    listener,
-                                    LocalFolder.this,
-                                    "SELECT " + GET_MESSAGES_COLS + " " +
-                                    "FROM messages " +
-                                    "WHERE deleted = 0 AND (empty IS NULL OR empty != 1) AND " +
-                                    "(thread_root = ? OR id = ?) AND folder_id = ?",
-                                    new String[] {
-                                        threadIdString,
-                                        threadIdString,
-                                        Long.toString(mFolderId)
-                                    });
-                        } catch (MessagingException e) {
-                            throw new WrappedException(e);
-                        }
-                    }
-                });
-            } catch (WrappedException e) {
-                throw(MessagingException) e.getCause();
-            }
-        }
-
         @Override
         public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
         throws MessagingException {
@@ -2125,11 +2053,11 @@ public class LocalStore extends Store implements Serializable {
 
                                     cv.clear();
                                     cv.put("thread_root", id);
-                                    int x = db.update("messages", cv, "thread_root = ?", oldIdArg);
+                                    db.update("messages", cv, "thread_root = ?", oldIdArg);
 
                                     cv.clear();
                                     cv.put("thread_parent", id);
-                                    x = db.update("messages", cv, "thread_parent = ?", oldIdArg);
+                                    db.update("messages", cv, "thread_parent = ?", oldIdArg);
                                 }
 
                                 /*
@@ -3303,23 +3231,11 @@ 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;
-        private boolean mEmpty;
-        private int mThreadCount = 0;
 
         public LocalMessage() {
         }
@@ -3377,12 +3293,6 @@ public class LocalStore extends Store implements Serializable {
 
             mRootId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
             mParentId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
-
-            mEmpty = (cursor.isNull(17)) ? false : (cursor.getInt(17) == 1);
-
-            if (cursor.getColumnCount() > 18) {
-                mThreadCount = cursor.getInt(18);
-            }
         }
 
         /**
@@ -3522,64 +3432,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);
         }
@@ -3886,10 +3738,6 @@ 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;
 
@@ -3903,14 +3751,6 @@ public class LocalStore extends Store implements Serializable {
         public long getParentId() {
             return mParentId;
         }
-
-        public boolean isEmpty() {
-            return mEmpty;
-        }
-
-        public int getThreadCount() {
-            return mThreadCount;
-        }
     }
 
     public static class LocalAttachmentBodyPart extends MimeBodyPart {

From c7a2080b34abcfc3f4f25c3d31552a2bccbeb511 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 04:34:41 +0200
Subject: [PATCH 32/74] Only show message list footer when displaying a single
 folder

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 12 ++++++++----
 1 file changed, 8 insertions(+), 4 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 9657b9802..a87c08ed8 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -763,8 +763,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mCurrentFolder = getFolder(mFolderName, mAccount);
         }
 
-        // Hide "Load up to x more" footer for search views
-        mFooterView.setVisibility((!mSingleFolderMode) ? View.GONE : View.VISIBLE);
+        if (mSingleFolderMode) {
+            mListView.addFooterView(getFooterView(mListView));
+            updateFooterView();
+        }
 
         mController = MessagingController.getInstance(getActivity().getApplication());
         mListView.setAdapter(mAdapter);
@@ -916,8 +918,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         mListView.setFastScrollEnabled(true);
         mListView.setScrollingCacheEnabled(false);
         mListView.setOnItemClickListener(this);
-        mListView.addFooterView(getFooterView(mListView));
-        //mListView.setChoiceMode(AbsListView.CHOICE_MODE_MULTIPLE);
 
         registerForContextMenu(mListView);
     }
@@ -1778,6 +1778,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);

From 508e9e8aa669075852a2e5906d3613177fcd0730 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 05:28:38 +0200
Subject: [PATCH 33/74] Don't display threaded message list for filtered views

---
 src/com/fsck/k9/activity/Accounts.java        |  6 ++--
 src/com/fsck/k9/activity/FolderList.java      |  4 +--
 .../fsck/k9/activity/LauncherShortcuts.java   |  3 +-
 src/com/fsck/k9/activity/MessageList.java     | 28 ++++++++++++++-----
 .../k9/controller/MessagingController.java    |  4 +--
 .../k9/provider/UnreadWidgetProvider.java     |  3 +-
 6 files changed, 32 insertions(+), 16 deletions(-)

diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index 9d6482a64..f45f908e3 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -620,7 +620,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
     private boolean onOpenAccount(BaseAccount account) {
         if (account instanceof SearchAccount) {
             SearchAccount searchAccount = (SearchAccount)account;
-            MessageList.actionDisplaySearch(this, searchAccount.getRelatedSearch(), false);
+            MessageList.actionDisplaySearch(this, searchAccount.getRelatedSearch(), false, false);
         } else {
             Account realAccount = (Account)account;
             if (!realAccount.isEnabled()) {
@@ -640,7 +640,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
                 LocalSearch search = new LocalSearch(realAccount.getAutoExpandFolderName());
                 search.addAllowedFolder(realAccount.getAutoExpandFolderName());
                 search.addAccountUuid(realAccount.getUuid());
-                MessageList.actionDisplaySearch(this, search, true);}
+                MessageList.actionDisplaySearch(this, search, false, true);}
         }
         return true;
     }
@@ -1797,7 +1797,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
 
             search.allRequiredFlags(searchModifier.requiredFlags);
             search.allForbiddenFlags(searchModifier.forbiddenFlags);
-            MessageList.actionDisplaySearch(Accounts.this, search, false);
+            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 487e8f44e..28eab3ec7 100644
--- a/src/com/fsck/k9/activity/FolderList.java
+++ b/src/com/fsck/k9/activity/FolderList.java
@@ -625,7 +625,7 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
         LocalSearch search = new LocalSearch(folder);
         search.addAccountUuid(mAccount.getUuid());
         search.addAllowedFolder(folder);
-        MessageList.actionDisplaySearch(this, search, false);
+        MessageList.actionDisplaySearch(this, search, false, false);
     }
 
     private void onCompact(Account account) {
@@ -1276,7 +1276,7 @@ public class FolderList extends K9ListActivity implements OnNavigationListener {
             }
             search.addAllowedFolder(folderName);
             search.addAccountUuid(account.getUuid());
-            MessageList.actionDisplaySearch(FolderList.this, search, false);
+            MessageList.actionDisplaySearch(FolderList.this, search, true, false);
         }
     }
 
diff --git a/src/com/fsck/k9/activity/LauncherShortcuts.java b/src/com/fsck/k9/activity/LauncherShortcuts.java
index f302051dc..94e6a4ff8 100644
--- a/src/com/fsck/k9/activity/LauncherShortcuts.java
+++ b/src/com/fsck/k9/activity/LauncherShortcuts.java
@@ -31,7 +31,8 @@ public class LauncherShortcuts extends AccountList {
         Intent shortcutIntent = null;
 
         if (account instanceof SearchSpecification) {
-            shortcutIntent = MessageList.intentDisplaySearch(this, (SearchSpecification) account, true, true);
+            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 020e07110..67f065da8 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -49,22 +49,28 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
     // 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";
 
-    public static void actionDisplaySearch(Context context, SearchSpecification search, boolean newTask) {
-        actionDisplaySearch(context, search, newTask, true);
+    public static void actionDisplaySearch(Context context, SearchSpecification search,
+            boolean noThreading, boolean newTask) {
+        actionDisplaySearch(context, search, noThreading, newTask, true);
     }
 
-    public static void actionDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) {
-        context.startActivity(intentDisplaySearch(context, search, newTask, clearTop));
+    public static void actionDisplaySearch(Context context, SearchSpecification search,
+            boolean noThreading, boolean newTask, boolean clearTop) {
+        context.startActivity(
+                intentDisplaySearch(context, search, noThreading, newTask, clearTop));
     }
 
-    public static Intent intentDisplaySearch(Context context, SearchSpecification search, boolean newTask, boolean clearTop) {
+    public static Intent intentDisplaySearch(Context context, SearchSpecification search,
+            boolean noThreading, boolean newTask, boolean clearTop) {
         Intent intent = new Intent(context, MessageList.class);
         intent.putExtra(EXTRA_SEARCH, search);
+        intent.putExtra(EXTRA_NO_THREADING, noThreading);
 
         if (clearTop) {
             intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
@@ -95,6 +101,13 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
     private boolean mIsRemote;
     private boolean mThreadViewEnabled = true;  //TODO: this should be a setting
 
+    /**
+     * {@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);
@@ -115,8 +128,8 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
         if (mMessageListFragment == null) {
             FragmentTransaction ft = fragmentManager.beginTransaction();
-            mMessageListFragment = MessageListFragment.newInstance(mSearch, mThreadViewEnabled,
-                    mIsRemote);
+            mMessageListFragment = MessageListFragment.newInstance(mSearch,
+                    (mThreadViewEnabled && !mNoThreading), mIsRemote);
             ft.add(R.id.message_list_container, mMessageListFragment);
             ft.commit();
         }
@@ -143,6 +156,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         } else {
             // regular LocalSearch object was passed
             mSearch = intent.getParcelableExtra(EXTRA_SEARCH);
+            mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false);
         }
 
         String[] accounts = mSearch.getAccountUuids();
diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 2c3ee24ea..4d0dcf2cc 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -3053,7 +3053,7 @@ public class MessagingController implements Runnable {
         LocalSearch search = new LocalSearch(account.getInboxFolderName());
         search.addAllowedFolder(account.getInboxFolderName());
         search.addAccountUuid(account.getUuid());
-        Intent intent = MessageList.intentDisplaySearch(mApplication, search, true, true);
+        Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
 
         PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
         builder.setContentIntent(pi);
@@ -3139,7 +3139,7 @@ public class MessagingController implements Runnable {
         LocalSearch search = new LocalSearch(account.getInboxFolderName());
         search.addAllowedFolder(account.getInboxFolderName());
         search.addAccountUuid(account.getUuid());
-        Intent intent = MessageList.intentDisplaySearch(mApplication, search, true, true);
+        Intent intent = MessageList.intentDisplaySearch(mApplication, search, false, true, true);
 
         PendingIntent pi = PendingIntent.getActivity(mApplication, 0, intent, 0);
         builder.setContentIntent(pi);
diff --git a/src/com/fsck/k9/provider/UnreadWidgetProvider.java b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
index 180b8c22a..75acd1073 100644
--- a/src/com/fsck/k9/provider/UnreadWidgetProvider.java
+++ b/src/com/fsck/k9/provider/UnreadWidgetProvider.java
@@ -66,7 +66,8 @@ public class UnreadWidgetProvider extends AppWidgetProvider {
                     LocalSearch search = new LocalSearch(account.getAutoExpandFolderName());
                     search.addAllowedFolder(account.getAutoExpandFolderName());
                     search.addAccountUuid(account.getUuid());
-                    clickIntent = MessageList.intentDisplaySearch(context, search, true, true);
+                    clickIntent = MessageList.intentDisplaySearch(context, search, false, true,
+                            true);
                 }
                 clickIntent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
             }

From 613ef6ced253561508ae287a449a18cac02950ae Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 05:35:45 +0200
Subject: [PATCH 34/74] "Select all" now won't start selection mode when there
 are no messages

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index a87c08ed8..8a1c5e03b 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1810,6 +1810,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private void setSelectionState(boolean selected) {
         if (selected) {
             mSelectedCount = mAdapter.getCount();
+            if (mSelectedCount == 0) {
+                // Nothing to do if there are no messages
+                return;
+            }
+
             for (int i = 0, end = mSelectedCount; i < end; i++) {
                 mSelected.put(i, true);
             }

From d703286833a03b692ec55f115fe3a14fde2561c2 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 06:01:26 +0200
Subject: [PATCH 35/74] Make LocalStore.searchForMessages() skip empty messages

---
 src/com/fsck/k9/mail/store/LocalStore.java | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 459015064..db9e52495 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -976,8 +976,10 @@ public class LocalStore extends Store implements Serializable {
         }
 
         // build sql query
-        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE deleted = 0 "
-                        + (search.getConditions() != null ? "AND (" + search.getConditions() + ")" : "") + " ORDER BY date DESC";
+        ConditionsTreeNode conditions = search.getConditions();
+        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE " +
+                "((empty IS NULL OR empty != 1) AND deleted = 0)" +
+                ((conditions != null) ? " AND (" + conditions + ")" : "") + " ORDER BY date DESC";
 
         if (K9.DEBUG) {
             Log.d(K9.LOG_TAG, "Query = " + sqlQuery);

From a4585657d115f46e506cbdb73643c01933b83408 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 24 Oct 2012 06:57:14 +0200
Subject: [PATCH 36/74] Fix global search

---
 src/com/fsck/k9/activity/MessageList.java | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 67f065da8..92ab7d2e4 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -140,17 +140,22 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         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));
+                mNoThreading = true;
+
+                mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query));
+                mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query));
+
                 Bundle appData = getIntent().getBundleExtra(SearchManager.APP_DATA);
                 if (appData != null) {
-                    mSearch = new LocalSearch();
                     mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT));
                     mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER));
 
-                    String query = intent.getStringExtra(SearchManager.QUERY);
-                    mSearch.or(new SearchCondition(Searchfield.SENDER, Attribute.CONTAINS, query));
-                    mSearch.or(new SearchCondition(Searchfield.SUBJECT, Attribute.CONTAINS, query));
-
                     mIsRemote = true;
+                } else {
+                    mSearch.addAccountUuid(LocalSearch.ALL_ACCOUNTS);
                 }
             }
         } else {

From 49a5a3b7ff733aa945e4efa4c035a5b447288857 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 25 Oct 2012 21:51:14 +0200
Subject: [PATCH 37/74] Fix message sorting for message lists of a single
 account

To support sorting of message lists spanning multiple accounts (e.g.
Unified Inbox) we need a MergeCursor that also does sorting.
---
 .../fsck/k9/fragment/MessageListFragment.java | 77 +++++++++++++------
 src/com/fsck/k9/provider/EmailProvider.java   | 12 +--
 2 files changed, 61 insertions(+), 28 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 8a1c5e03b..d61de81b0 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -413,17 +413,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * perform the operation in the calling thread.

*/ class MessageListHandler extends Handler { - private static final int ACTION_SORT_MESSAGES = 1; - private static final int ACTION_FOLDER_LOADING = 2; - private static final int ACTION_REFRESH_TITLE = 3; - private static final int ACTION_PROGRESS = 4; + private static final int ACTION_FOLDER_LOADING = 1; + private static final int ACTION_REFRESH_TITLE = 2; + private static final int ACTION_PROGRESS = 3; - 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); @@ -454,10 +448,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void handleMessage(android.os.Message msg) { switch (msg.what) { - case ACTION_SORT_MESSAGES: { - mAdapter.sortMessages(); - break; - } case ACTION_FOLDER_LOADING: { String folder = (String) msg.obj; boolean loading = (msg.arg1 == 1); @@ -1024,7 +1014,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() { @@ -1451,7 +1444,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick if (updateForMe(account, folder)) { mHandler.progress(false); mHandler.folderLoading(folder, false); - mHandler.sortMessages(); } super.synchronizeMailboxFinished(account, folder, totalMessagesInMailbox, numNewMessages); } @@ -1462,7 +1454,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick if (updateForMe(account, folder)) { mHandler.progress(false); mHandler.folderLoading(folder, false); - mHandler.sortMessages(); } super.synchronizeMailboxFailed(account, folder, message); } @@ -1513,12 +1504,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick //notifyDataSetChanged(); } - public void sortMessages() { - //TODO: implement - - //notifyDataSetChanged(); - } - private String recipientSigil(boolean toMe, boolean ccMe) { if (toMe) { return getString(R.string.messagelist_sent_to_me_sigil); @@ -2667,8 +2652,54 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick String selection = query.toString(); String[] selectionArgs = queryArgs.toArray(new String[0]); + 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; + break; + } + case SORT_UNREAD: { + sortColumn = "(" + MessageColumns.FLAGS + " LIKE '%SEEN%')"; + break; + } + case SORT_DATE: + default: { + sortColumn = MessageColumns.DATE; + } + } + + String sortDirection; + String secondarySort; + if (mSortType == SortType.SORT_DATE) { + sortDirection = (mSortDateAscending) ? " ASC" : " DESC"; + secondarySort = ""; + } else { + sortDirection = (mSortAscending) ? " ASC" : " DESC"; + secondarySort = MessageColumns.DATE + " DESC, "; + } + + String sortOrder = sortColumn + sortDirection + ", " + secondarySort + + MessageColumns.ID + " DESC"; + return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs, - MessageColumns.DATE + " DESC"); + sortOrder); } private void buildQuery(Account account, ConditionsTreeNode node, StringBuilder query, diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java index 55fff0e96..75f3a3b06 100644 --- a/src/com/fsck/k9/provider/EmailProvider.java +++ b/src/com/fsck/k9/provider/EmailProvider.java @@ -219,12 +219,14 @@ public class EmailProvider extends ContentProvider { String where; if (StringUtils.isNullOrEmpty(selection)) { - where = InternalMessageColumns.DELETED + "=0 AND " + - InternalMessageColumns.EMPTY + "!=1"; + where = InternalMessageColumns.DELETED + "=0 AND (" + + InternalMessageColumns.EMPTY + " IS NULL OR " + + InternalMessageColumns.EMPTY + "!=1)"; } else { where = "(" + selection + ") AND " + - InternalMessageColumns.DELETED + "=0 AND " + - InternalMessageColumns.EMPTY + "!=1"; + InternalMessageColumns.DELETED + "=0 AND (" + + InternalMessageColumns.EMPTY + " IS NULL OR " + + InternalMessageColumns.EMPTY + "!=1)"; } return db.query(MESSAGES_TABLE, projection, where, selectionArgs, null, null, @@ -288,7 +290,7 @@ public class EmailProvider extends ContentProvider { if (!StringUtils.isNullOrEmpty(sortOrder)) { query.append(" ORDER BY "); - query.append(sortOrder); + query.append(addPrefixToSelection(MESSAGES_COLUMNS, "m.", sortOrder)); } return db.rawQuery(query.toString(), selectionArgs); From 08fba5468fca507e003e7698feab55cbb0bb5f1f Mon Sep 17 00:00:00 2001 From: cketti Date: Thu, 25 Oct 2012 23:14:28 +0200 Subject: [PATCH 38/74] Switch MergeCursor from using a List to an array --- .../fsck/k9/fragment/MessageListFragment.java | 17 +- src/com/fsck/k9/helper/MergeCursor.java | 187 ++++++++++-------- .../k9/helper/MergeCursorWithUniqueId.java | 6 +- 3 files changed, 114 insertions(+), 96 deletions(-) diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index d61de81b0..c58fd1df7 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -338,7 +338,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private String[] mAccountUuids; private int mUnreadMessageCount = 0; - private Map mCursors = new HashMap(); + private Cursor[] mCursors; /** * Stores the name of the folder that we want to open as soon as possible @@ -700,7 +700,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick initializeMessageList(); LoaderManager loaderManager = getLoaderManager(); - for (int i = 0, len = mAccountUuids.length; i < len; i++) { + int len = mAccountUuids.length; + mCursors = new Cursor[len]; + for (int i = 0; i < len; i++) { loaderManager.initLoader(i, null, this); } } @@ -2757,16 +2759,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void onLoadFinished(Loader loader, Cursor data) { - mCursors.put(loader.getId(), data); + mCursors[loader.getId()] = data; - List list = new LinkedList(mCursors.keySet()); - Collections.sort(list); - List cursors = new ArrayList(list.size()); - for (Integer id : list) { - cursors.add(mCursors.get(id)); - } - - MergeCursorWithUniqueId cursor = new MergeCursorWithUniqueId(cursors); + MergeCursorWithUniqueId cursor = new MergeCursorWithUniqueId(mCursors); mSelected = new SparseBooleanArray(cursor.getCount()); //TODO: use the (stable) IDs as index and reuse the old mSelected diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java index efba70d23..1edc6360c 100644 --- a/src/com/fsck/k9/helper/MergeCursor.java +++ b/src/com/fsck/k9/helper/MergeCursor.java @@ -1,7 +1,23 @@ +/* + * 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.List; - +import android.annotation.TargetApi; import android.content.ContentResolver; import android.database.CharArrayBuffer; import android.database.ContentObserver; @@ -18,7 +34,7 @@ public class MergeCursor implements Cursor { /** * List of the cursors combined in this object. */ - protected final List mCursors; + protected final Cursor[] mCursors; /** * The currently active cursor. @@ -32,6 +48,8 @@ public class MergeCursor implements Cursor { */ protected int mActiveCursorIndex; + protected int mPosition; + /** * Used to cache the value of {@link #getCount()} */ @@ -44,16 +62,24 @@ public class MergeCursor implements Cursor { * @param cursors * The list of cursors this {@code MultiCursor} should combine. */ - public MergeCursor(List cursors) { - mCursors = cursors; - mActiveCursorIndex = 0; - mActiveCursor = cursors.get(0); + public MergeCursor(Cursor[] cursors) { + mCursors = cursors.clone(); + + for (int i = 0, len = mCursors.length; i < len; i++) { + if (mCursors[i] != null) { + mActiveCursorIndex = i; + mActiveCursor = mCursors[mActiveCursorIndex]; + } + } + mPosition = -1; } @Override public void close() { for (Cursor cursor : mCursors) { - cursor.close(); + if (cursor != null) { + cursor.close(); + } } } @@ -65,7 +91,9 @@ public class MergeCursor implements Cursor { @Override public void deactivate() { for (Cursor cursor : mCursors) { - cursor.deactivate(); + if (cursor != null) { + cursor.deactivate(); + } } } @@ -105,7 +133,9 @@ public class MergeCursor implements Cursor { if (mCount == -1) { int count = 0; for (Cursor cursor : mCursors) { - count += cursor.getCount(); + if (cursor != null) { + count += cursor.getCount(); + } } mCount = count; @@ -136,12 +166,7 @@ public class MergeCursor implements Cursor { @Override public int getPosition() { - int pos = 0; - for (int i = 0; i < mActiveCursorIndex; i++) { - pos += mCursors.get(i).getCount(); - } - - return pos + mActiveCursor.getPosition(); + return mPosition; } @Override @@ -154,6 +179,7 @@ public class MergeCursor implements Cursor { return mActiveCursor.getString(columnIndex); } + @TargetApi(11) @Override public int getType(int columnIndex) { return mActiveCursor.getType(columnIndex); @@ -166,20 +192,21 @@ public class MergeCursor implements Cursor { @Override public boolean isAfterLast() { - if (mActiveCursorIndex == mCursors.size() - 1) { - return mActiveCursor.isAfterLast(); + int count = getCount(); + if (count == 0) { + return true; } - return false; + return (mPosition == count); } @Override public boolean isBeforeFirst() { - if (mActiveCursorIndex == 0) { - return mActiveCursor.isBeforeFirst(); + if (getCount() == 0) { + return true; } - return false; + return (mPosition == -1); } @Override @@ -189,20 +216,21 @@ public class MergeCursor implements Cursor { @Override public boolean isFirst() { - if (mActiveCursorIndex == 0) { - return mActiveCursor.isFirst(); + if (getCount() == 0) { + return false; } - return false; + return (mPosition == 0); } @Override public boolean isLast() { - if (mActiveCursorIndex == mCursors.size() - 1) { - return mActiveCursor.isLast(); + int count = getCount(); + if (count == 0) { + return false; } - return false; + return (mPosition == (count - 1)); } @Override @@ -212,81 +240,78 @@ public class MergeCursor implements Cursor { @Override public boolean move(int offset) { - int ofs = offset; - int pos = mActiveCursor.getPosition(); - if (offset >= 0) { - while (pos + ofs > mActiveCursor.getCount() & - mActiveCursorIndex < mCursors.size() - 1) { - - // Adjust the "move offset" - ofs -= mActiveCursor.getCount() - pos; - - // Move to the next cursor - mActiveCursor = mCursors.get(++mActiveCursorIndex); - - // Move the new cursor to the first position - mActiveCursor.moveToFirst(); - pos = 0; - } - } else { - while (pos + ofs < 0 && mActiveCursorIndex > 0) { - // Adjust the "move offset" - ofs += pos; - - // Move to the next cursor - mActiveCursor = mCursors.get(--mActiveCursorIndex); - - // Move the new cursor to the first position - mActiveCursor.moveToLast(); - pos = mActiveCursor.getPosition(); - } - } - - return mActiveCursor.move(ofs); + return moveToPosition(mPosition + offset); } @Override public boolean moveToFirst() { - mActiveCursorIndex = 0; - mActiveCursor = mCursors.get(mActiveCursorIndex); - return mActiveCursor.moveToFirst(); + return moveToPosition(0); } @Override public boolean moveToLast() { - mActiveCursorIndex = mCursors.size() - 1; - mActiveCursor = mCursors.get(mActiveCursorIndex); - return mActiveCursor.moveToLast(); + return moveToPosition(getCount() - 1); } @Override public boolean moveToNext() { - return move(1); + return moveToPosition(mPosition + 1); } @Override public boolean moveToPosition(int position) { - // Start at the beginning - mActiveCursorIndex = 0; - mActiveCursor = mCursors.get(mActiveCursorIndex); - - int pos = position; - while (pos > mActiveCursor.getCount() - 1 && - mActiveCursorIndex < mCursors.size() - 1) { - - // Adjust the position - pos -= mActiveCursor.getCount(); - - // Move to the next cursor - mActiveCursor = mCursors.get(++mActiveCursorIndex); + // Make sure position isn't past the end of the cursor + final int count = getCount(); + if (position >= count) { + mPosition = count; + return false; } - return mActiveCursor.moveToPosition(pos); + // 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; + } + + /* Find the right cursor */ + mActiveCursor = null; + mActiveCursorIndex = -1; + mPosition = -1; + int cursorStartPos = 0; + + for (int i = 0, len = mCursors.length; i < len; i++) { + if (mCursors[i] == null) { + continue; + } + + if (position < (cursorStartPos + mCursors[i].getCount())) { + mActiveCursorIndex = i; + mActiveCursor = mCursors[mActiveCursorIndex]; + break; + } + + cursorStartPos += mCursors[i].getCount(); + } + + /* Move it to the right position */ + if (mActiveCursor != null) { + boolean success = mActiveCursor.moveToPosition(position - cursorStartPos); + mPosition = (success) ? position : -1; + + return success; + } + + return false; } @Override public boolean moveToPrevious() { - return move(-1); + return moveToPosition(mPosition - 1); } @Override diff --git a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java index 284fcf9f4..7cad2b084 100644 --- a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java +++ b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java @@ -1,7 +1,5 @@ package com.fsck.k9.helper; -import java.util.List; - import android.database.Cursor; @@ -14,10 +12,10 @@ public class MergeCursorWithUniqueId extends MergeCursor { private int mIdColumnIndex = -1; - public MergeCursorWithUniqueId(List cursors) { + public MergeCursorWithUniqueId(Cursor[] cursors) { super(cursors); - if (cursors.size() > MAX_CURSORS) { + if (cursors.length > MAX_CURSORS) { throw new IllegalArgumentException("This class only supports up to " + MAX_CURSORS + " cursors"); } From d74ca8c8ce869ad63465551507e12668ef8faee8 Mon Sep 17 00:00:00 2001 From: cketti Date: Fri, 26 Oct 2012 02:44:40 +0200 Subject: [PATCH 39/74] Move MessageInfoHolder comparator to MessageProvider The MessageInfoHolder comparators aren't used in MessageListFragment anymore. --- src/com/fsck/k9/provider/MessageProvider.java | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/com/fsck/k9/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java index d40ed23e0..967fe79cf 100644 --- a/src/com/fsck/k9/provider/MessageProvider.java +++ b/src/com/fsck/k9/provider/MessageProvider.java @@ -23,10 +23,8 @@ import com.fsck.k9.K9; import com.fsck.k9.Preferences; 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; @@ -38,6 +36,7 @@ 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; @@ -308,8 +307,7 @@ public class MessageProvider extends ContentProvider { 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) { @@ -1123,4 +1121,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); + } + } + } } From faa666394ce0bf9e4239869ee194991d2f3308f1 Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 27 Oct 2012 02:15:30 +0200 Subject: [PATCH 40/74] Do sorting in MergeCursor when merging the query results Disabled "sort by sender" for now because the database can't sort by contact names from the contacts database. We probably have to special-case that and do in-memory sorting. --- res/menu/message_list_option.xml | 2 + src/com/fsck/k9/Account.java | 2 +- src/com/fsck/k9/activity/MessageList.java | 8 +- .../fsck/k9/fragment/MessageListFragment.java | 250 ++++++++++-------- src/com/fsck/k9/helper/MergeCursor.java | 157 ++++++++--- .../k9/helper/MergeCursorWithUniqueId.java | 6 +- 6 files changed, 270 insertions(+), 155 deletions(-) diff --git a/res/menu/message_list_option.xml b/res/menu/message_list_option.xml index 106dfab5a..ed0a09078 100644 --- a/res/menu/message_list_option.xml +++ b/res/menu/message_list_option.xml @@ -29,9 +29,11 @@ + diff --git a/src/com/fsck/k9/Account.java b/src/com/fsck/k9/Account.java index 6fe63d3ba..5ce0d3f96 100644 --- a/src/com/fsck/k9/Account.java +++ b/src/com/fsck/k9/Account.java @@ -94,7 +94,7 @@ public class Account implements BaseAccount { SORT_DATE(R.string.sort_earliest_first, R.string.sort_latest_first, false), SORT_ARRIVAL(R.string.sort_earliest_first, R.string.sort_latest_first, false), SORT_SUBJECT(R.string.sort_subject_alpha, R.string.sort_subject_re_alpha, true), - SORT_SENDER(R.string.sort_sender_alpha, R.string.sort_sender_re_alpha, true), +// SORT_SENDER(R.string.sort_sender_alpha, R.string.sort_sender_re_alpha, true), SORT_UNREAD(R.string.sort_unread_first, R.string.sort_unread_last, true), SORT_FLAGGED(R.string.sort_flagged_first, R.string.sort_flagged_last, true), SORT_ATTACHMENT(R.string.sort_attach_first, R.string.sort_unattached_first, true); diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index 92ab7d2e4..bf8f296a9 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -374,10 +374,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; diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index c58fd1df7..9b0dd87e0 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -7,8 +7,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.EnumMap; -import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.concurrent.Future; @@ -69,7 +67,6 @@ 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.fragment.ConfirmationDialogFragment; @@ -77,7 +74,6 @@ import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmen 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; @@ -173,7 +169,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick // arg1 & 2 are mixed up, this is done on purpose return mDelegate.compare(object2, object1); } - } /** @@ -182,7 +177,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @param */ public static class ComparatorChain implements Comparator { - private List> mChain; /** @@ -204,89 +198,93 @@ 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); - } - } @@ -301,17 +299,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick /** * 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()); @@ -471,35 +469,33 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick * @return The comparator to use to display messages in an ordered * fashion. Never 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) { @@ -699,6 +695,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick initializeMessageList(); + // This needs to be done before initializing the cursor loader below + initializeSortSettings(); + LoaderManager loaderManager = getLoaderManager(); int len = mAccountUuids.length; mCursors = new Cursor[len]; @@ -707,6 +706,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } + 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(); @@ -781,11 +792,24 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } 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.getName(); + return localFolder; } catch (Exception e) { Log.e(K9.LOG_TAG, "getFolderNameById() failed.", e); return null; @@ -879,17 +903,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick Account[] accountsWithNotification; 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 = mPreferences.getAccounts(); - mSortType = K9.getSortType(); - mSortAscending = K9.isSortAscending(mSortType); - mSortDateAscending = K9.isSortAscending(SortType.SORT_DATE); } for (Account accountWithNotification : accountsWithNotification) { @@ -1145,10 +1162,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; @@ -2496,8 +2513,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition); String uid = cursor.getString(UID_COLUMN); - //TODO: get account and folder from cursor - Folder folder = mCurrentFolder.folder; + Account account = getAccountFromCursor(cursor); + long folderId = cursor.getLong(FOLDER_ID_COLUMN); + Folder folder = getFolderById(account, folderId); try { return folder.getMessage(uid); @@ -2668,13 +2686,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick sortColumn = "(" + MessageColumns.FLAGS + " NOT LIKE '%FLAGGED%')"; break; } - case SORT_SENDER: { - //FIXME - sortColumn = MessageColumns.SENDER_LIST; - break; - } +// case SORT_SENDER: { +// //FIXME +// sortColumn = MessageColumns.SENDER_LIST; +// break; +// } case SORT_SUBJECT: { - sortColumn = MessageColumns.SUBJECT; + sortColumn = MessageColumns.SUBJECT + " COLLATE NOCASE"; break; } case SORT_UNREAD: { @@ -2687,14 +2705,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick } } - String sortDirection; + String sortDirection = (mSortAscending) ? " ASC" : " DESC"; String secondarySort; - if (mSortType == SortType.SORT_DATE) { - sortDirection = (mSortDateAscending) ? " ASC" : " DESC"; + if (mSortType == SortType.SORT_DATE || mSortType == SortType.SORT_ARRIVAL) { secondarySort = ""; } else { - sortDirection = (mSortAscending) ? " ASC" : " DESC"; - secondarySort = MessageColumns.DATE + " DESC, "; + secondarySort = MessageColumns.DATE + ((mSortDateAscending) ? " ASC, " : " DESC, "); } String sortOrder = sortColumn + sortDirection + ", " + secondarySort + @@ -2759,9 +2775,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick @Override public void onLoadFinished(Loader loader, Cursor data) { - mCursors[loader.getId()] = data; - - MergeCursorWithUniqueId cursor = new MergeCursorWithUniqueId(mCursors); + Cursor cursor; + if (mCursors.length > 1) { + mCursors[loader.getId()] = data; + cursor = new MergeCursorWithUniqueId(mCursors, getComparator()); + } else { + cursor = data; + } mSelected = new SparseBooleanArray(cursor.getCount()); //TODO: use the (stable) IDs as index and reuse the old mSelected diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java index 1edc6360c..39776e646 100644 --- a/src/com/fsck/k9/helper/MergeCursor.java +++ b/src/com/fsck/k9/helper/MergeCursor.java @@ -17,6 +17,8 @@ package com.fsck.k9.helper; +import java.util.Comparator; + import android.annotation.TargetApi; import android.content.ContentResolver; import android.database.CharArrayBuffer; @@ -48,30 +50,53 @@ public class MergeCursor implements Cursor { */ protected int mActiveCursorIndex; + /** + * The cursor's current position. + */ protected int mPosition; /** - * Used to cache the value of {@link #getCount()} + * 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) { + 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++) { - if (mCursors[i] != null) { - mActiveCursorIndex = i; - mActiveCursor = mCursors[mActiveCursorIndex]; + Cursor cursor = mCursors[i]; + if (cursor != null) { + cursor.moveToPosition(-1); + + if (mActiveCursor == null) { + mActiveCursorIndex = i; + mActiveCursor = mCursors[mActiveCursorIndex]; + } } } - mPosition = -1; } @Override @@ -255,7 +280,50 @@ public class MergeCursor implements Cursor { @Override public boolean moveToNext() { - return moveToPosition(mPosition + 1); + 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].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 @@ -278,40 +346,63 @@ public class MergeCursor implements Cursor { return true; } - /* Find the right cursor */ - mActiveCursor = null; - mActiveCursorIndex = -1; - mPosition = -1; - int cursorStartPos = 0; - - for (int i = 0, len = mCursors.length; i < len; i++) { - if (mCursors[i] == null) { - continue; + if (position > mPosition) { + for (int i = 0, end = position - mPosition; i < end; i++) { + if (!moveToNext()) { + return false; + } } - - if (position < (cursorStartPos + mCursors[i].getCount())) { - mActiveCursorIndex = i; - mActiveCursor = mCursors[mActiveCursorIndex]; - break; + } else { + for (int i = 0, end = mPosition - position; i < end; i++) { + if (!moveToPrevious()) { + return false; + } } - - cursorStartPos += mCursors[i].getCount(); } - /* Move it to the right position */ - if (mActiveCursor != null) { - boolean success = mActiveCursor.moveToPosition(position - cursorStartPos); - mPosition = (success) ? position : -1; - - return success; - } - - return false; + return true; } @Override public boolean moveToPrevious() { - return moveToPosition(mPosition - 1); + 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 diff --git a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java index 7cad2b084..111d49fbb 100644 --- a/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java +++ b/src/com/fsck/k9/helper/MergeCursorWithUniqueId.java @@ -1,5 +1,7 @@ package com.fsck.k9.helper; +import java.util.Comparator; + import android.database.Cursor; @@ -12,8 +14,8 @@ public class MergeCursorWithUniqueId extends MergeCursor { private int mIdColumnIndex = -1; - public MergeCursorWithUniqueId(Cursor[] cursors) { - super(cursors); + public MergeCursorWithUniqueId(Cursor[] cursors, Comparator comparator) { + super(cursors, comparator); if (cursors.length > MAX_CURSORS) { throw new IllegalArgumentException("This class only supports up to " + From 65b3a5734056a4941f444001825b186d2380037b Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 27 Oct 2012 04:48:37 +0200 Subject: [PATCH 41/74] Clone LocalSearch object before modifying it for unread/starred search --- src/com/fsck/k9/activity/Accounts.java | 3 +- .../fsck/k9/search/ConditionsTreeNode.java | 29 +++++++++++++++++++ src/com/fsck/k9/search/LocalSearch.java | 9 ++++++ .../fsck/k9/search/SearchSpecification.java | 5 ++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java index f45f908e3..f9d02d5a7 100644 --- a/src/com/fsck/k9/activity/Accounts.java +++ b/src/com/fsck/k9/activity/Accounts.java @@ -64,7 +64,6 @@ import com.fsck.k9.Preferences; import com.fsck.k9.R; 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; @@ -1788,7 +1787,7 @@ public class Accounts extends K9ListActivity implements OnItemClickListener { LocalSearch search = null; if (account instanceof SearchAccount) { - search = ((SearchAccount) account).getRelatedSearch(); + search = ((SearchAccount) account).getRelatedSearch().clone(); search.setName(description); } else { search = new LocalSearch(description); diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java index 430f31255..52bc6bb58 100644 --- a/src/com/fsck/k9/search/ConditionsTreeNode.java +++ b/src/com/fsck/k9/search/ConditionsTreeNode.java @@ -124,6 +124,35 @@ public class ConditionsTreeNode implements Parcelable { } + /* 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 /////////////////////////////////////////////////////////////// diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java index c52dec469..a93ce187f 100644 --- a/src/com/fsck/k9/search/LocalSearch.java +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -80,6 +80,15 @@ public class LocalSearch implements SearchSpecification { } } + @Override + public LocalSearch clone() { + ConditionsTreeNode conditions = (mConditions == null) ? null : mConditions.cloneTree(); + + LocalSearch copy = new LocalSearch(mName, conditions, null, mPredefined); + copy.mAccountUuids = new HashSet(mAccountUuids); + + return copy; + } /////////////////////////////////////////////////////////////// // Public manipulation methods diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java index 8f22622c6..a953a5d4d 100644 --- a/src/com/fsck/k9/search/SearchSpecification.java +++ b/src/com/fsck/k9/search/SearchSpecification.java @@ -134,6 +134,11 @@ public interface SearchSpecification extends Parcelable { this.field = Searchfield.values()[in.readInt()]; } + @Override + public SearchCondition clone() { + return new SearchCondition(field, attribute, value); + } + public String toHumanString() { return field.toString() + attribute.toString(); } From 889e2502be50b53bbb1c7d9112b6d2ff31c843eb Mon Sep 17 00:00:00 2001 From: cketti Date: Sat, 27 Oct 2012 05:09:58 +0200 Subject: [PATCH 42/74] Replace call to method only available with API 9 and higher --- .../fsck/k9/fragment/MessageListFragment.java | 4 ++-- src/com/fsck/k9/helper/Utility.java | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 9b0dd87e0..fee3f8739 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -2,7 +2,6 @@ package com.fsck.k9.fragment; import java.text.DateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; @@ -74,6 +73,7 @@ import com.fsck.k9.fragment.ConfirmationDialogFragment.ConfirmationDialogFragmen 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; @@ -133,7 +133,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick private static final int ACCOUNT_UUID_COLUMN = 14; private static final int THREAD_COUNT_COLUMN = 15; - private static final String[] PROJECTION = Arrays.copyOf(THREADED_PROJECTION, + private static final String[] PROJECTION = Utility.copyOf(THREADED_PROJECTION, THREAD_COUNT_COLUMN); diff --git a/src/com/fsck/k9/helper/Utility.java b/src/com/fsck/k9/helper/Utility.java index 5a66a0ab9..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; @@ -19,6 +21,7 @@ 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; @@ -708,4 +711,17 @@ public class Utility { 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; + } } From 5778d135fbf6dc434fd0cb673eea7cc2d9b7814e Mon Sep 17 00:00:00 2001 From: cketti Date: Sun, 28 Oct 2012 20:10:52 +0100 Subject: [PATCH 43/74] Add global setting to enable/disable threaded view --- res/values/strings.xml | 3 +++ res/xml/global_preferences.xml | 6 ++++++ src/com/fsck/k9/K9.java | 12 ++++++++++++ src/com/fsck/k9/activity/MessageList.java | 11 +++++------ src/com/fsck/k9/activity/setup/Prefs.java | 10 ++++++++++ src/com/fsck/k9/fragment/MessageListFragment.java | 2 +- src/com/fsck/k9/preferences/GlobalSettings.java | 3 +++ src/com/fsck/k9/preferences/Settings.java | 2 +- src/com/fsck/k9/search/LocalSearch.java | 9 +++++++++ 9 files changed, 50 insertions(+), 8 deletions(-) diff --git a/res/values/strings.xml b/res/values/strings.xml index 448cc7b44..3bf56d49c 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -1150,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 12fb56f5b..ee9b02dd6 100644 --- a/res/xml/global_preferences.xml +++ b/res/xml/global_preferences.xml @@ -144,6 +144,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. @@ -472,6 +474,7 @@ public class K9 extends Application { editor.putString("attachmentdefaultpath", mAttachmentDefaultPath); editor.putBoolean("useBackgroundAsUnreadIndicator", sUseBackgroundAsUnreadIndicator); + editor.putBoolean("threadedView", sThreadedViewEnabled); fontSizes.save(editor); } @@ -641,6 +644,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 { @@ -1127,4 +1131,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/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java index bf8f296a9..8402cebdd 100644 --- a/src/com/fsck/k9/activity/MessageList.java +++ b/src/com/fsck/k9/activity/MessageList.java @@ -99,7 +99,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme private boolean mSingleFolderMode; private boolean mSingleAccountMode; private boolean mIsRemote; - private boolean mThreadViewEnabled = true; //TODO: this should be a setting /** * {@code true} if the message list should be displayed as flat list (i.e. no threading) @@ -129,7 +128,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme if (mMessageListFragment == null) { FragmentTransaction ft = fragmentManager.beginTransaction(); mMessageListFragment = MessageListFragment.newInstance(mSearch, - (mThreadViewEnabled && !mNoThreading), mIsRemote); + (K9.isThreadedViewEnabled() && !mNoThreading), mIsRemote); ft.add(R.id.message_list_container, mMessageListFragment); ft.commit(); } @@ -164,13 +163,13 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme mNoThreading = intent.getBooleanExtra(EXTRA_NO_THREADING, false); } - String[] accounts = mSearch.getAccountUuids(); - mSingleAccountMode = ( accounts != null && accounts.length == 1 - && !accounts[0].equals(SearchSpecification.ALL_ACCOUNTS)); + String[] accountUuids = mSearch.getAccountUuids(); + mSingleAccountMode = (accountUuids.length == 1 && !mSearch.searchAllAccounts()); mSingleFolderMode = mSingleAccountMode && (mSearch.getFolderNames().size() == 1); if (mSingleAccountMode) { - mAccount = Preferences.getPreferences(this).getAccount(accounts[0]); + 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"); diff --git a/src/com/fsck/k9/activity/setup/Prefs.java b/src/com/fsck/k9/activity/setup/Prefs.java index 588de7701..1fa7f939f 100644 --- a/src/com/fsck/k9/activity/setup/Prefs.java +++ b/src/com/fsck/k9/activity/setup/Prefs.java @@ -86,8 +86,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; @@ -126,6 +129,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); @@ -230,6 +235,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 { @@ -412,6 +421,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/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index fee3f8739..1438c33fe 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -729,7 +729,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick String[] accountUuids = mSearch.getAccountUuids(); mSingleAccountMode = false; - if (accountUuids.length == 1 && !accountUuids[0].equals(SearchSpecification.ALL_ACCOUNTS)) { + if (accountUuids.length == 1 && !mSearch.searchAllAccounts()) { mSingleAccountMode = true; mAccount = mPreferences.getAccount(accountUuids[0]); } diff --git a/src/com/fsck/k9/preferences/GlobalSettings.java b/src/com/fsck/k9/preferences/GlobalSettings.java index e49face93..4b2cd52c7 100644 --- a/src/com/fsck/k9/preferences/GlobalSettings.java +++ b/src/com/fsck/k9/preferences/GlobalSettings.java @@ -219,6 +219,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/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java index a93ce187f..444e9adde 100644 --- a/src/com/fsck/k9/search/LocalSearch.java +++ b/src/com/fsck/k9/search/LocalSearch.java @@ -356,6 +356,15 @@ public class LocalSearch implements SearchSpecification { 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. * From 303c1ee85d3d8983554e8db8853935cef6238755 Mon Sep 17 00:00:00 2001 From: cketti Date: Mon, 29 Oct 2012 02:27:34 +0100 Subject: [PATCH 44/74] Extract generation of SQL queries for searches to SqlQueryBuilder --- .../fsck/k9/fragment/MessageListFragment.java | 69 +----- src/com/fsck/k9/mail/store/LocalStore.java | 29 +-- .../fsck/k9/search/ConditionsTreeNode.java | 12 - src/com/fsck/k9/search/LocalSearch.java | 1 + .../fsck/k9/search/SearchSpecification.java | 80 ++----- src/com/fsck/k9/search/SqlQueryBuilder.java | 225 ++++++++++++++++++ 6 files changed, 272 insertions(+), 144 deletions(-) create mode 100644 src/com/fsck/k9/search/SqlQueryBuilder.java diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java index 1438c33fe..ad668d339 100644 --- a/src/com/fsck/k9/fragment/MessageListFragment.java +++ b/src/com/fsck/k9/fragment/MessageListFragment.java @@ -85,10 +85,9 @@ 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.ConditionsTreeNode; import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.SearchSpecification; -import com.fsck.k9.search.SearchSpecification.SearchCondition; +import com.fsck.k9.search.SqlQueryBuilder; import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshListView; @@ -2667,11 +2666,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick StringBuilder query = new StringBuilder(); List queryArgs = new ArrayList(); - buildQuery(account, mSearch.getConditions(), query, queryArgs); + 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: { @@ -2715,62 +2721,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick String sortOrder = sortColumn + sortDirection + ", " + secondarySort + MessageColumns.ID + " DESC"; - - return new CursorLoader(getActivity(), uri, projection, selection, selectionArgs, - sortOrder); - } - - private void buildQuery(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; - //TODO: Fix the search condition used by the Unified Inbox - if (LocalSearch.GENERIC_INBOX_NAME.equals(condition.value) || - "1".equals(condition.value)) { - folderName = account.getInboxFolderName(); - } else { - folderName = condition.value; - } - long folderId = getFolderId(account, folderName); - query.append("folder_id = ?"); - selectionArgs.add(Long.toString(folderId)); - break; - } - default: { - query.append(condition.toString()); - } - } - } else { - query.append("("); - buildQuery(account, node.mLeft, query, selectionArgs); - query.append(") "); - query.append(node.mValue.name()); - query.append(" ("); - buildQuery(account, node.mRight, query, selectionArgs); - query.append(")"); - } - } - - private long getFolderId(Account account, String folderName) { - long folderId = 0; - try { - LocalFolder folder = (LocalFolder) getFolder(folderName, account).folder; - folder.open(OpenMode.READ_ONLY); - folderId = folder.getId(); - } catch (MessagingException e) { - //FIXME - e.printStackTrace(); - } - - return folderId; + return sortOrder; } @Override diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java index db9e52495..0a25a2b52 100644 --- a/src/com/fsck/k9/mail/store/LocalStore.java +++ b/src/com/fsck/k9/mail/store/LocalStore.java @@ -46,6 +46,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,9 +72,8 @@ 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.ConditionsTreeNode; import com.fsck.k9.search.LocalSearch; -import com.fsck.k9.search.SearchSpecification.Searchfield; +import com.fsck.k9.search.SqlQueryBuilder; /** *
@@ -960,32 +960,23 @@ public class LocalStore extends Store implements Serializable {
     public Message[] searchForMessages(MessageRetrievalListener retrievalListener,
                                         LocalSearch search) throws MessagingException {
 
-        // update some references in the search that have to be bound to this one store
-        for (ConditionsTreeNode node : search.getLeafSet()) {
-            if (node.mCondition.field == Searchfield.FOLDER) {
-                // TODO find better solution
-                if (isFolderId(node.mCondition.value)) {
-                    continue;
-                }
+        StringBuilder query = new StringBuilder();
+        List queryArgs = new ArrayList();
+        SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs);
 
-                if (node.mCondition.value.equals(LocalSearch.GENERIC_INBOX_NAME)) {
-                    node.mCondition.value = mAccount.getInboxFolderName();
-                }
-                node.mCondition.value = String.valueOf(getFolderId(node.mCondition.value));
-            }
-        }
+        String where = query.toString();
+        String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
 
-        // build sql query
-        ConditionsTreeNode conditions = search.getConditions();
         String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE " +
                 "((empty IS NULL OR empty != 1) AND deleted = 0)" +
-                ((conditions != null) ? " AND (" + conditions + ")" : "") + " ORDER BY date DESC";
+                ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
+                " ORDER BY date DESC";
 
         if (K9.DEBUG) {
             Log.d(K9.LOG_TAG, "Query = " + sqlQuery);
         }
 
-        return getMessages(retrievalListener, null, sqlQuery, new String[] {});
+        return getMessages(retrievalListener, null, sqlQuery, selectionArgs);
     }
 
     /*
diff --git a/src/com/fsck/k9/search/ConditionsTreeNode.java b/src/com/fsck/k9/search/ConditionsTreeNode.java
index 52bc6bb58..1a1be85d7 100644
--- a/src/com/fsck/k9/search/ConditionsTreeNode.java
+++ b/src/com/fsck/k9/search/ConditionsTreeNode.java
@@ -239,18 +239,6 @@ public class ConditionsTreeNode implements Parcelable {
         return mCondition;
     }
 
-
-    /**
-     * This will traverse the tree inorder and call toString recursively resulting
-     * in a valid SQL where clause.
-     */
-    @Override
-    public String toString() {
-        return (mLeft == null ? "" : "(" + mLeft + ")")
-                + " " + ( mCondition == null ? mValue.name() : mCondition ) + " "
-                + (mRight == null ? "" : "(" + mRight + ")") ;
-    }
-
     /**
      * Get a set of all the leaves in the tree.
      * @return Set of all the leaves.
diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index 444e9adde..ac3463258 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -112,6 +112,7 @@ public class LocalSearch implements SearchSpecification {
     public void addAccountUuid(String uuid) {
         if (uuid.equals(ALL_ACCOUNTS)) {
             mAccountUuids.clear();
+            return;
         }
         mAccountUuids.add(uuid);
     }
diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
index a953a5d4d..f40f1dfba 100644
--- a/src/com/fsck/k9/search/SearchSpecification.java
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -35,40 +35,17 @@ public interface SearchSpecification extends Parcelable {
     // ATTRIBUTE enum
     ///////////////////////////////////////////////////////////////
     public enum Attribute {
-        CONTAINS(false), EQUALS(false), STARTSWITH(false), ENDSWITH(false),
-        NOT_CONTAINS(true), NOT_EQUALS(true), NOT_STARTSWITH(true), NOT_ENDSWITH(true);
+        CONTAINS,
+        NOT_CONTAINS,
 
-        private boolean mNegation;
+        EQUALS,
+        NOT_EQUALS,
 
-        private Attribute(boolean negation) {
-            this.mNegation = negation;
-        }
+        STARTSWITH,
+        NOT_STARTSWITH,
 
-        public String formQuery(String value) {
-            String queryPart = "";
-
-            switch (this) {
-            case NOT_CONTAINS:
-            case CONTAINS:
-                queryPart = "'%"+value+"%'";
-                break;
-            case NOT_EQUALS:
-            case EQUALS:
-                queryPart = "'"+value+"'";
-                break;
-            case NOT_STARTSWITH:
-            case STARTSWITH:
-                queryPart = "'%"+value+"'";
-                break;
-            case NOT_ENDSWITH:
-            case ENDSWITH:
-                queryPart = "'"+value+"%'";
-                break;
-            default: queryPart = "'"+value+"'";
-            }
-
-            return (mNegation ? " NOT LIKE " : " LIKE ") + queryPart;
-        }
+        ENDSWITH,
+        NOT_ENDSWITH
     }
 
     ///////////////////////////////////////////////////////////////
@@ -87,21 +64,21 @@ public interface SearchSpecification extends Parcelable {
      *
      */
     public enum Searchfield {
-        SUBJECT("subject"), DATE("date"), UID("uid"), FLAG("flags"),
-        SENDER("sender_list"), TO("to_list"), CC("cc_list"), FOLDER("folder_id"),
-        BCC("bcc_list"), REPLY_TO("reply_to_list"), MESSAGE("text_content"),
-        ATTACHMENT_COUNT("attachment_count"), DELETED("deleted"), THREAD_ROOT("thread_root"),
-        ID("id");
-
-        private String dbName;
-
-        private Searchfield(String dbName) {
-            this.dbName = dbName;
-        }
-
-        public String getDatabaseName() {
-            return dbName;
-        }
+        SUBJECT,
+        DATE,
+        UID,
+        FLAG,
+        SENDER,
+        TO,
+        CC,
+        FOLDER,
+        BCC,
+        REPLY_TO,
+        MESSAGE_CONTENTS,
+        ATTACHMENT_COUNT,
+        DELETED,
+        THREAD_ROOT,
+        ID
     }
 
 
@@ -118,9 +95,9 @@ public interface SearchSpecification extends Parcelable {
      * @author dzan
      */
     public class SearchCondition implements Parcelable {
-        public String value;
-        public Attribute attribute;
-        public Searchfield field;
+        public final String value;
+        public final Attribute attribute;
+        public final Searchfield field;
 
         public SearchCondition(Searchfield field, Attribute attribute, String value) {
             this.value = value;
@@ -143,11 +120,6 @@ public interface SearchSpecification extends Parcelable {
             return field.toString() + attribute.toString();
         }
 
-        @Override
-        public String toString() {
-            return field.getDatabaseName() + attribute.formQuery(value);
-        }
-
         @Override
         public boolean equals(Object o) {
             if (o instanceof SearchCondition) {
diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
new file mode 100644
index 000000000..3749c3507
--- /dev/null
+++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
@@ -0,0 +1,225 @@
+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;
+                    //TODO: Fix the search condition used by the Unified Inbox (we search all
+                    // folders with an enabled "Unify" folder setting).
+                    if (LocalSearch.GENERIC_INBOX_NAME.equals(condition.value)) {
+                        folderName = account.getInboxFolderName();
+                    } else {
+                        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;
+            }
+        }
+
+        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 THREAD_ROOT: {
+                return true;
+            }
+            default: {
+                return false;
+            }
+        }
+    }
+}

From 968d948dd098f032f3d1a47a613ab96388932f21 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 30 Oct 2012 16:27:09 +0100
Subject: [PATCH 45/74] Restore show next/previous message functionality in
 MessageView

This is just a temporary fix. In the future we want to get rid of
passing the serialized message list from MessageList to MessageView.
---
 .../fsck/k9/fragment/MessageListFragment.java | 17 +++---
 src/com/fsck/k9/provider/EmailProvider.java   | 54 +++++++++++++++++--
 2 files changed, 60 insertions(+), 11 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index ad668d339..2cd5201c3 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -111,6 +111,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         MessageColumns.THREAD_ROOT,
         MessageColumns.THREAD_PARENT,
         SpecialColumns.ACCOUNT_UUID,
+        SpecialColumns.FOLDER_NAME,
 
         MessageColumns.THREAD_COUNT,
     };
@@ -130,7 +131,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private static final int THREAD_ROOT_COLUMN = 12;
     private static final int THREAD_PARENT_COLUMN = 13;
     private static final int ACCOUNT_UUID_COLUMN = 14;
-    private static final int THREAD_COUNT_COLUMN = 15;
+    private static final int FOLDER_NAME_COLUMN = 15;
+    private static final int THREAD_COUNT_COLUMN = 16;
 
     private static final String[] PROJECTION = Utility.copyOf(THREADED_PROJECTION,
             THREAD_COUNT_COLUMN);
@@ -2440,13 +2442,16 @@ 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);
         }
-        */
-        //TODO: implement
 
         return messageRefs;
     }
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index 75f3a3b06..07b2ee1fa 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -8,6 +8,7 @@ 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;
@@ -36,8 +37,6 @@ import android.net.Uri;
  */
 /*
  * TODO:
- * - modify MessagingController (or LocalStore?) to call ContentResolver.notifyChange() to trigger
- *   notifications when the underlying data changes.
  * - add support for account list and folder list
  */
 public class EmailProvider extends ContentProvider {
@@ -94,6 +93,8 @@ public class EmailProvider extends ContentProvider {
 
     public interface SpecialColumns {
         public static final String ACCOUNT_UUID = "account_uuid";
+
+        public static final String FOLDER_NAME = "name";
     }
 
     public interface MessageColumns {
@@ -229,8 +230,42 @@ public class EmailProvider extends ContentProvider {
                                 InternalMessageColumns.EMPTY + "!=1)";
                     }
 
-                    return db.query(MESSAGES_TABLE, projection, where, selectionArgs, null, null,
-                            sortOrder);
+                    final Cursor cursor;
+                    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(addPrefixToSelection(MESSAGES_COLUMNS, "m.", where));
+                        query.append(" ORDER BY ");
+                        query.append(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) {
@@ -264,6 +299,9 @@ public class EmailProvider extends ContentProvider {
                             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 {
                             query.append("m.");
                             query.append(columnName);
@@ -274,7 +312,13 @@ public class EmailProvider extends ContentProvider {
 
                     query.append(
                             " FROM messages h JOIN messages m " +
-                            "ON (h.id = m.thread_root OR h.id = m.id) " +
+                            "ON (h.id = m.thread_root OR h.id = m.id) ");
+
+                    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 " +

From 0baffd05c87847864deaf07372fc36b21c6921ca Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 30 Oct 2012 16:54:21 +0100
Subject: [PATCH 46/74] Hide remote search UI elements

Remote search is currently not working
---
 .../fsck/k9/activity/setup/AccountSettings.java | 17 ++++++++++++++++-
 .../fsck/k9/fragment/MessageListFragment.java   |  3 ++-
 2 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java
index f18a1912c..0bd7bd1b2 100644
--- a/src/com/fsck/k9/activity/setup/AccountSettings.java
+++ b/src/com/fsck/k9/activity/setup/AccountSettings.java
@@ -181,9 +181,12 @@ public class AccountSettings extends K9PreferenceActivity {
     private CheckBoxPreference mCryptoAutoEncrypt;
 
     private PreferenceScreen mSearchScreen;
+    //FIXME: Remote search is temporarily disabled
+    /*
     private CheckBoxPreference mCloudSearchEnabled;
     private ListPreference mRemoteSearchNumResults;
     private CheckBoxPreference mRemoteSearchFullText;
+    */
 
     private ListPreference mLocalStorageProvider;
     private ListPreference mArchiveFolder;
@@ -500,6 +503,10 @@ public class AccountSettings extends K9PreferenceActivity {
         // IMAP-specific preferences
 
         mSearchScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_SEARCH);
+
+        //FIXME: Remote search is temporarily disabled
+        mMainScreen.removePreference(mSearchScreen);
+        /*
         mCloudSearchEnabled = (CheckBoxPreference) findPreference(PREFERENCE_CLOUD_SEARCH_ENABLED);
         mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS);
         mRemoteSearchNumResults.setOnPreferenceChangeListener(
@@ -512,6 +519,7 @@ public class AccountSettings extends K9PreferenceActivity {
         );
         updateRemoteSearchLimit(mRemoteSearchNumResults.getValue());
         mRemoteSearchFullText = (CheckBoxPreference) findPreference(PREFERENCE_REMOTE_SEARCH_FULL_TEXT);
+        */
 
         mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT);
         mIdleRefreshPeriod = (ListPreference) findPreference(PREFERENCE_IDLE_REFRESH_PERIOD);
@@ -519,9 +527,12 @@ public class AccountSettings extends K9PreferenceActivity {
         if (mIsPushCapable) {
             mPushPollOnConnect.setChecked(mAccount.isPushPollOnConnect());
 
+            //FIXME: Remote search is temporarily disabled
+            /*
             mCloudSearchEnabled.setChecked(mAccount.allowRemoteSearch());
             mRemoteSearchNumResults.setValue(Integer.toString(mAccount.getRemoteSearchNumResults()));
             mRemoteSearchFullText.setChecked(mAccount.isRemoteSearchFullText());
+            */
 
             mIdleRefreshPeriod.setValue(String.valueOf(mAccount.getIdleRefreshMinutes()));
             mIdleRefreshPeriod.setSummary(mIdleRefreshPeriod.getEntry());
@@ -795,9 +806,12 @@ public class AccountSettings extends K9PreferenceActivity {
             mAccount.setPushPollOnConnect(mPushPollOnConnect.isChecked());
             mAccount.setIdleRefreshMinutes(Integer.parseInt(mIdleRefreshPeriod.getValue()));
             mAccount.setMaxPushFolders(Integer.parseInt(mMaxPushFolders.getValue()));
+            //FIXME: Remote search is temporarily disabled
+            /*
             mAccount.setAllowRemoteSearch(mCloudSearchEnabled.isChecked());
             mAccount.setRemoteSearchNumResults(Integer.parseInt(mRemoteSearchNumResults.getValue()));
             mAccount.setRemoteSearchFullText(mRemoteSearchFullText.isChecked());
+            */
         }
 
         if (!mIsMoveCapable) {
@@ -982,7 +996,8 @@ public class AccountSettings extends K9PreferenceActivity {
                 maxResults = getString(R.string.account_settings_remote_search_num_results_entries_all);
             }
 
-            mRemoteSearchNumResults.setSummary(String.format(getString(R.string.account_settings_remote_search_num_summary), maxResults));
+            //FIXME: Remote search is temporarily disabled
+            //mRemoteSearchNumResults.setSummary(String.format(getString(R.string.account_settings_remote_search_num_summary), maxResults));
         }
     }
 
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 2cd5201c3..902bc170b 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -144,7 +144,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         Bundle args = new Bundle();
         args.putParcelable(ARG_SEARCH, search);
         args.putBoolean(ARG_THREADED_LIST, threadedList);
-        args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
+        //FIXME: Remote search temporarily disabled
+        //args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
         fragment.setArguments(args);
         return fragment;
     }

From b0ef68dfedb6ed994959e0ab63077673715842a4 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Tue, 30 Oct 2012 19:24:19 +0100
Subject: [PATCH 47/74] Make search also search the message contents

---
 src/com/fsck/k9/activity/MessageList.java | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 8402cebdd..12dedf3c1 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -146,6 +146,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
 
                 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) {

From a54666e0201cf56adc6b635cc76fff1e286269b0 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 31 Oct 2012 01:06:26 +0100
Subject: [PATCH 48/74] Fix edge case in MergeCursor

---
 src/com/fsck/k9/helper/MergeCursor.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/helper/MergeCursor.java b/src/com/fsck/k9/helper/MergeCursor.java
index 39776e646..3325a6e36 100644
--- a/src/com/fsck/k9/helper/MergeCursor.java
+++ b/src/com/fsck/k9/helper/MergeCursor.java
@@ -293,7 +293,7 @@ public class MergeCursor implements Cursor {
 
         int smallest = -1;
         for (int i = 0, len = mCursors.length; i < len; i++) {
-            if (mCursors[i] == null || mCursors[i].isLast()) {
+            if (mCursors[i] == null || mCursors[i].getCount() == 0 || mCursors[i].isLast()) {
                 continue;
             }
 

From 08b361ed3693ccb472f754bbab29fa0cc1b34fc6 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 31 Oct 2012 01:45:44 +0100
Subject: [PATCH 49/74] Fix search for Unified Inbox

We don't want to list the Inbox contents of all accounts but the
contents of folders whose "Unify" setting is true.
---
 src/com/fsck/k9/activity/Accounts.java          | 16 ++++------------
 src/com/fsck/k9/mail/store/LocalStore.java      |  6 +++---
 src/com/fsck/k9/provider/EmailProvider.java     |  6 ++++++
 src/com/fsck/k9/search/SearchAccount.java       |  4 +++-
 src/com/fsck/k9/search/SearchSpecification.java |  4 ++--
 src/com/fsck/k9/search/SqlQueryBuilder.java     | 16 +++++++---------
 6 files changed, 25 insertions(+), 27 deletions(-)

diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index f9d02d5a7..ea11d1738 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -81,6 +81,8 @@ import com.fsck.k9.search.LocalSearch;
 import com.fsck.k9.search.SearchAccount;
 import com.fsck.k9.search.SearchModifier;
 import com.fsck.k9.search.SearchSpecification;
+import com.fsck.k9.search.SearchSpecification.Attribute;
+import com.fsck.k9.search.SearchSpecification.Searchfield;
 import com.fsck.k9.view.ColorChip;
 import com.fsck.k9.preferences.SettingsExporter;
 import com.fsck.k9.preferences.SettingsImportExportException;
@@ -425,18 +427,8 @@ public class Accounts extends K9ListActivity implements OnItemClickListener {
      * Creates and initializes the special accounts ('Unified Inbox' and 'All Messages')
      */
     private void createSpecialAccounts() {
-        // create the unified inbox meta account ( all accounts is default when none specified )
-        String name = getString(R.string.integrated_inbox_title);
-        LocalSearch tmpSearch = new LocalSearch(name);
-        tmpSearch.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME);
-        integratedInboxAccount = new SearchAccount(tmpSearch, name,
-                getString(R.string.integrated_inbox_detail));
-
-        // create the all messages search ( all accounts is default when none specified )
-        name = getString(R.string.search_all_messages_title);
-        tmpSearch = new LocalSearch(name);
-        unreadAccount = new SearchAccount(tmpSearch, name,
-                getString(R.string.search_all_messages_detail));
+        integratedInboxAccount = SearchAccount.createUnifiedInboxAccount(this);
+        unreadAccount = SearchAccount.createAllMessagesAccount(this);
     }
 
     @SuppressWarnings("unchecked")
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 0a25a2b52..487ed7917 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -93,10 +93,9 @@ public class LocalStore extends Store implements Serializable {
      * in the correct order.
      */
     static private String GET_MESSAGES_COLS =
-        "subject, sender_list, date, uid, flags, id, to_list, cc_list, "
+        "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";
 
 
@@ -967,7 +966,8 @@ public class LocalStore extends Store implements Serializable {
         String where = query.toString();
         String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
 
-        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE " +
+        String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
+                "LEFT JOIN folders ON (folders.id = messages.id) WHERE " +
                 "((empty IS NULL OR empty != 1) AND deleted = 0)" +
                 ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
                 " ORDER BY date DESC";
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index 07b2ee1fa..38ebb9cdc 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -95,6 +95,7 @@ public class EmailProvider extends ContentProvider {
         public static final String ACCOUNT_UUID = "account_uuid";
 
         public static final String FOLDER_NAME = "name";
+        public static final String INTEGRATE = "integrate";
     }
 
     public interface MessageColumns {
@@ -231,6 +232,7 @@ public class EmailProvider extends ContentProvider {
                     }
 
                     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 ");
@@ -302,6 +304,9 @@ public class EmailProvider extends ContentProvider {
                         } 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);
@@ -314,6 +319,7 @@ public class EmailProvider extends ContentProvider {
                             " 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) ");
                     }
diff --git a/src/com/fsck/k9/search/SearchAccount.java b/src/com/fsck/k9/search/SearchAccount.java
index 201b7eb0a..1eb20a7b5 100644
--- a/src/com/fsck/k9/search/SearchAccount.java
+++ b/src/com/fsck/k9/search/SearchAccount.java
@@ -6,6 +6,8 @@ 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
@@ -26,7 +28,7 @@ public class SearchAccount implements BaseAccount {
     public static SearchAccount createUnifiedInboxAccount(Context context) {
         String name = context.getString(R.string.integrated_inbox_title);
         LocalSearch tmpSearch = new LocalSearch(name);
-        tmpSearch.addAllowedFolder(SearchSpecification.GENERIC_INBOX_NAME);
+        tmpSearch.and(Searchfield.INTEGRATE, "1", Attribute.EQUALS);
         return new SearchAccount(tmpSearch, name,
                 context.getString(R.string.integrated_inbox_detail));
     }
diff --git a/src/com/fsck/k9/search/SearchSpecification.java b/src/com/fsck/k9/search/SearchSpecification.java
index f40f1dfba..1e599aed9 100644
--- a/src/com/fsck/k9/search/SearchSpecification.java
+++ b/src/com/fsck/k9/search/SearchSpecification.java
@@ -29,7 +29,6 @@ public interface SearchSpecification extends Parcelable {
      * Some meta names for certain conditions.
      */
     public static final String ALL_ACCOUNTS = "allAccounts";
-    public static final String GENERIC_INBOX_NAME = "genericInboxName";
 
     ///////////////////////////////////////////////////////////////
     // ATTRIBUTE enum
@@ -78,7 +77,8 @@ public interface SearchSpecification extends Parcelable {
         ATTACHMENT_COUNT,
         DELETED,
         THREAD_ROOT,
-        ID
+        ID,
+        INTEGRATE
     }
 
 
diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
index 3749c3507..478ecc7da 100644
--- a/src/com/fsck/k9/search/SqlQueryBuilder.java
+++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
@@ -27,14 +27,7 @@ public class SqlQueryBuilder {
             SearchCondition condition = node.mCondition;
             switch (condition.field) {
                 case FOLDER: {
-                    String folderName;
-                    //TODO: Fix the search condition used by the Unified Inbox (we search all
-                    // folders with an enabled "Unify" folder setting).
-                    if (LocalSearch.GENERIC_INBOX_NAME.equals(condition.value)) {
-                        folderName = account.getInboxFolderName();
-                    } else {
-                        folderName = condition.value;
-                    }
+                    String folderName = condition.value;
                     long folderId = getFolderId(account, folderName);
                     query.append("folder_id = ?");
                     selectionArgs.add(Long.toString(folderId));
@@ -139,6 +132,10 @@ public class SqlQueryBuilder {
                 columnName = "uid";
                 break;
             }
+            case INTEGRATE: {
+                columnName = "integrate";
+                break;
+            }
         }
 
         if (columnName == null) {
@@ -191,7 +188,7 @@ public class SqlQueryBuilder {
             }
             case EQUALS: {
                 if (isNumberColumn(field)) {
-                    query.append("== ?");
+                    query.append("= ?");
                 } else {
                     query.append("LIKE ?");
                 }
@@ -214,6 +211,7 @@ public class SqlQueryBuilder {
             case DELETED:
             case FOLDER:
             case ID:
+            case INTEGRATE:
             case THREAD_ROOT: {
                 return true;
             }

From 60bf3e7e29169ebbab1b90ddc38ff0cbaad3dbfb Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 31 Oct 2012 01:49:11 +0100
Subject: [PATCH 50/74] Rename class members

---
 src/com/fsck/k9/activity/Accounts.java | 19 ++++++++-----------
 1 file changed, 8 insertions(+), 11 deletions(-)

diff --git a/src/com/fsck/k9/activity/Accounts.java b/src/com/fsck/k9/activity/Accounts.java
index ea11d1738..6254ff245 100644
--- a/src/com/fsck/k9/activity/Accounts.java
+++ b/src/com/fsck/k9/activity/Accounts.java
@@ -80,9 +80,6 @@ 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.search.SearchSpecification;
-import com.fsck.k9.search.SearchSpecification.Attribute;
-import com.fsck.k9.search.SearchSpecification.Searchfield;
 import com.fsck.k9.view.ColorChip;
 import com.fsck.k9.preferences.SettingsExporter;
 import com.fsck.k9.preferences.SettingsImportExportException;
@@ -129,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;
@@ -378,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])) {
@@ -427,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")
@@ -522,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);
         }

From 2e1d25118a3d256191dd1801521c94bde561e8d3 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 1 Nov 2012 20:33:13 +0100
Subject: [PATCH 51/74] Fix JOIN-condition when searching for messages

---
 src/com/fsck/k9/mail/store/LocalStore.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 487ed7917..9d8ca9d9b 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -967,7 +967,7 @@ public class LocalStore extends Store implements Serializable {
         String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
 
         String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
-                "LEFT JOIN folders ON (folders.id = messages.id) WHERE " +
+                "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";

From db3ea36978feb56013d24d5380df27f61eee4413 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Fri, 2 Nov 2012 05:18:24 +0100
Subject: [PATCH 52/74] Include number of messages in a thread in number of
 selected messages

Selecting a placeholder message will select all messages in its thread.
---
 .../fsck/k9/fragment/MessageListFragment.java | 37 +++++++++++++++----
 1 file changed, 29 insertions(+), 8 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 902bc170b..53b46b7e2 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1815,14 +1815,22 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      */
     private void setSelectionState(boolean selected) {
         if (selected) {
-            mSelectedCount = mAdapter.getCount();
-            if (mSelectedCount == 0) {
+            if (mAdapter.getCount() == 0) {
                 // Nothing to do if there are no messages
                 return;
             }
 
-            for (int i = 0, end = mSelectedCount; i < end; i++) {
+            mSelectedCount = 0;
+            for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
                 mSelected.put(i, true);
+
+                if (mThreadedList) {
+                    Cursor cursor = (Cursor) mAdapter.getItem(i);
+                    int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
+                    mSelectedCount += (threadCount > 1) ? threadCount : 1;
+                } else {
+                    mSelectedCount++;
+                }
             }
 
             if (mActionMode == null) {
@@ -1850,10 +1858,23 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         boolean selected = mSelected.get(adapterPosition, false);
-        mSelected.put(adapterPosition, !selected);
+        if (!selected) {
+            mSelected.put(adapterPosition, true);
+        } else {
+            mSelected.delete(adapterPosition);
+        }
+
+        int selectedCountDelta = 1;
+        if (mThreadedList) {
+            Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
+            int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
+            if (threadCount > 1) {
+                selectedCountDelta = threadCount;
+            }
+        }
 
         if (mActionMode != null) {
-            if (mSelectedCount == 1 && selected) {
+            if (mSelectedCount == selectedCountDelta && selected) {
                 mActionMode.finish();
                 mActionMode = null;
                 return;
@@ -1863,9 +1884,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (selected) {
-            mSelectedCount -= 1;
+            mSelectedCount -= selectedCountDelta;
         } else {
-            mSelectedCount += 1;
+            mSelectedCount += selectedCountDelta;
         }
 
         computeBatchDirection();
@@ -1884,7 +1905,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void computeSelectAllVisibility() {
-        mActionModeCallback.showSelectAll(mSelectedCount != mAdapter.getCount());
+        mActionModeCallback.showSelectAll(mSelected.size() != mAdapter.getCount());
     }
 
     private void computeBatchDirection() {

From c686284a77159982b9edc7723013d4fd262d9b32 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Fri, 2 Nov 2012 05:18:45 +0100
Subject: [PATCH 53/74] Remove unused method

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 10 ----------
 1 file changed, 10 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 53b46b7e2..328b69086 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1515,16 +1515,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mForwardedAnsweredIcon = getResources().getDrawable(R.drawable.ic_email_forwarded_answered_small);
         }
 
-        /**
-         * Set the selection state for all messages at once.
-         * @param selected Selection state to set.
-         */
-        public void setSelectionForAllMesages(final boolean selected) {
-            //TODO: implement
-
-            //notifyDataSetChanged();
-        }
-
         private String recipientSigil(boolean toMe, boolean ccMe) {
             if (toMe) {
                 return getString(R.string.messagelist_sent_to_me_sigil);

From 966794f169f9a88f311807dae822ff2966112094 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Fri, 2 Nov 2012 09:30:23 +0100
Subject: [PATCH 54/74] Don't create (throw-away) Message objects when starting
 action mode

---
 .../fsck/k9/fragment/MessageListFragment.java | 40 ++++++++++++++-----
 1 file changed, 30 insertions(+), 10 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 328b69086..9e9d00467 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -6,8 +6,10 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.EnumMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.Future;
 
 import android.app.Activity;
@@ -2218,23 +2220,41 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 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.
-                 */
-                Message[] messages = getCheckedMessages();
-                Account account;
+                Set accountUuids = getAccountUuidsForSelected();
 
-                for (Message message : messages) {
-                    account = 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++) {
+                if (mSelected.get(position, false)) {
+                    Cursor cursor = (Cursor) mAdapter.getItem(position);
+                    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;

From e339dd1f1098d5a2bc19ac0e20bb10307e497f09 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 01:52:45 +0100
Subject: [PATCH 55/74] Delete all messages in a thread when 'delete' is used
 on the placeholder

---
 .../k9/controller/MessagingController.java    | 41 +++++++++++++++++++
 .../fsck/k9/fragment/MessageListFragment.java |  8 +++-
 src/com/fsck/k9/mail/store/LocalStore.java    | 18 +++++++-
 src/com/fsck/k9/provider/EmailProvider.java   | 22 ++++------
 src/com/fsck/k9/search/SqlQueryBuilder.java   |  9 ++++
 5 files changed, 82 insertions(+), 16 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 4d0dcf2cc..7578591ab 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -3560,6 +3560,47 @@ public class MessagingController implements Runnable {
         }
     }
 
+    public void deleteThreads(final Message[] messages) {
+        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("deleteThreads", null, new Runnable() {
+                    @Override
+                    public void run() {
+                        deleteThreadsSynchronous(account, folder.getName(), accountMessages);
+                    }
+                });
+            }
+        });
+    }
+
+    public void deleteThreadsSynchronous(Account account, String folderName,
+            List messages) {
+
+        try {
+            LocalStore localStore = account.getLocalStore();
+
+            List messagesToDelete = new ArrayList();
+            for (Message message : messages) {
+                long rootId = ((LocalMessage) message).getRootId();
+                Message[] messagesInThread = localStore.getMessagesInThread(rootId);
+                Collections.addAll(messagesToDelete, messagesInThread);
+            }
+
+            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 void deleteMessages(final Message[] messages, final MessagingListener listener) {
         actOnMessages(messages, new MessageActor() {
 
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 9e9d00467..04b789d07 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1068,7 +1068,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onDelete(Message[] messages) {
-        mController.deleteMessages(messages, null);
+        if (mThreadedList) {
+            mController.deleteThreads(messages);
+        } else {
+            mController.deleteMessages(messages, null);
+        }
     }
 
     @Override
@@ -2563,7 +2567,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private Message[] getCheckedMessages() {
-        Message[] messages = new Message[mSelectedCount];
+        Message[] messages = new Message[mSelected.size()];
         int out = 0;
         for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
             if (mSelected.get(position, false)) {
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index 9d8ca9d9b..f4120408a 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -73,6 +73,9 @@ 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;
 
 /**
@@ -963,7 +966,10 @@ public class LocalStore extends Store implements Serializable {
         List queryArgs = new ArrayList();
         SqlQueryBuilder.buildWhereClause(mAccount, search.getConditions(), query, queryArgs);
 
-        String where = query.toString();
+        // 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 " +
@@ -1036,6 +1042,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
diff --git a/src/com/fsck/k9/provider/EmailProvider.java b/src/com/fsck/k9/provider/EmailProvider.java
index 38ebb9cdc..150c272dc 100644
--- a/src/com/fsck/k9/provider/EmailProvider.java
+++ b/src/com/fsck/k9/provider/EmailProvider.java
@@ -15,6 +15,7 @@ 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;
@@ -257,9 +258,11 @@ public class EmailProvider extends ContentProvider {
                         query.append(" FROM messages m " +
                                 "LEFT JOIN folders f ON (m.folder_id = f.id) " +
                                 "WHERE ");
-                        query.append(addPrefixToSelection(MESSAGES_COLUMNS, "m.", where));
+                        query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+                                "m.", where));
                         query.append(" ORDER BY ");
-                        query.append(addPrefixToSelection(MESSAGES_COLUMNS, "m.", sortOrder));
+                        query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+                                "m.", sortOrder));
 
                         cursor = db.rawQuery(query.toString(), selectionArgs);
                     } else {
@@ -332,7 +335,8 @@ public class EmailProvider extends ContentProvider {
 
                     if (!StringUtils.isNullOrEmpty(selection)) {
                         query.append("AND (");
-                        query.append(addPrefixToSelection(MESSAGES_COLUMNS, "h.", selection));
+                        query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+                                "h.", selection));
                         query.append(") ");
                     }
 
@@ -340,7 +344,8 @@ public class EmailProvider extends ContentProvider {
 
                     if (!StringUtils.isNullOrEmpty(sortOrder)) {
                         query.append(" ORDER BY ");
-                        query.append(addPrefixToSelection(MESSAGES_COLUMNS, "m.", sortOrder));
+                        query.append(SqlQueryBuilder.addPrefixToSelection(MESSAGES_COLUMNS,
+                                "m.", sortOrder));
                     }
 
                     return db.rawQuery(query.toString(), selectionArgs);
@@ -351,15 +356,6 @@ public class EmailProvider extends ContentProvider {
         }
     }
 
-    private 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;
-    }
-
     private Account getAccount(String accountUuid) {
         if (mPreferences == null) {
             Context appContext = getContext().getApplicationContext();
diff --git a/src/com/fsck/k9/search/SqlQueryBuilder.java b/src/com/fsck/k9/search/SqlQueryBuilder.java
index 478ecc7da..22d087ba0 100644
--- a/src/com/fsck/k9/search/SqlQueryBuilder.java
+++ b/src/com/fsck/k9/search/SqlQueryBuilder.java
@@ -220,4 +220,13 @@ public class SqlQueryBuilder {
             }
         }
     }
+
+    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;
+    }
 }

From fb5e8eea84912e9a31467c2fe9bca7e0ae0327fe Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 03:21:30 +0100
Subject: [PATCH 56/74] Fix computeBatchDirection()

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 04b789d07..b9b3f5594 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1908,13 +1908,15 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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++) {
+            if (mSelected.get(i, false)) {
+                Cursor cursor = (Cursor) mAdapter.getItem(i);
+                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;
                 }
 
@@ -1923,8 +1925,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                 }
             }
         }
-        */
-        //TODO: implement
 
         mActionModeCallback.showMarkAsRead(isBatchRead);
         mActionModeCallback.showFlag(isBatchFlag);

From 36b572fa5f369945bf5ed23d80433d0bd95193b5 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 04:25:40 +0100
Subject: [PATCH 57/74] Fix deleteThreadsSynchronous()

---
 src/com/fsck/k9/controller/MessagingController.java | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 7578591ab..7da30973c 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -3590,7 +3590,9 @@ public class MessagingController implements Runnable {
             List messagesToDelete = new ArrayList();
             for (Message message : messages) {
                 long rootId = ((LocalMessage) message).getRootId();
-                Message[] messagesInThread = localStore.getMessagesInThread(rootId);
+                long threadId = (rootId == -1) ? message.getId() : rootId;
+
+                Message[] messagesInThread = localStore.getMessagesInThread(threadId);
                 Collections.addAll(messagesToDelete, messagesInThread);
             }
 

From 7a266dcbdfe12a343fd54add084d7e16807cc09b Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 04:26:41 +0100
Subject: [PATCH 58/74] Retain selected messages when message list is reloaded

---
 .../fsck/k9/fragment/MessageListFragment.java | 59 +++++++++++++------
 1 file changed, 42 insertions(+), 17 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index b9b3f5594..a6f74d9d6 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -36,7 +36,6 @@ import android.text.SpannableStringBuilder;
 import android.text.style.AbsoluteSizeSpan;
 import android.text.style.ForegroundColorSpan;
 import android.util.Log;
-import android.util.SparseBooleanArray;
 import android.util.TypedValue;
 import android.view.ContextMenu;
 import android.view.ContextMenu.ContextMenuInfo;
@@ -367,7 +366,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private boolean mSenderAboveSubject = false;
 
     private int mSelectedCount = 0;
-    private SparseBooleanArray mSelected;
+    private Set mSelected = new HashSet();
 
     private FontSizes mFontSizes = K9.getFontSizes();
 
@@ -1630,8 +1629,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             int maybeBoldTypeface = (read) ? Typeface.NORMAL : Typeface.BOLD;
 
-            int adapterPosition = cursor.getPosition();
-            boolean selected = mSelected.get(adapterPosition, false);
+            long id = cursor.getLong(ID_COLUMN);
+            boolean selected = mSelected.contains(id);
 
             if (selected) {
                 holder.chip.setBackgroundDrawable(account.getCheckmarkChip().drawable());
@@ -1818,10 +1817,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             mSelectedCount = 0;
             for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
-                mSelected.put(i, true);
+                Cursor cursor = (Cursor) mAdapter.getItem(i);
+                long id = cursor.getLong(ID_COLUMN);
+                mSelected.add(id);
 
                 if (mThreadedList) {
-                    Cursor cursor = (Cursor) mAdapter.getItem(i);
                     int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
                     mSelectedCount += (threadCount > 1) ? threadCount : 1;
                 } else {
@@ -1853,16 +1853,18 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             return;
         }
 
-        boolean selected = mSelected.get(adapterPosition, false);
+        Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
+        long id = cursor.getLong(ID_COLUMN);
+
+        boolean selected = mSelected.contains(id);
         if (!selected) {
-            mSelected.put(adapterPosition, true);
+            mSelected.add(id);
         } else {
-            mSelected.delete(adapterPosition);
+            mSelected.remove(id);
         }
 
         int selectedCountDelta = 1;
         if (mThreadedList) {
-            Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
             int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
             if (threadCount > 1) {
                 selectedCountDelta = threadCount;
@@ -1909,8 +1911,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         boolean isBatchRead = false;
 
         for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
-            if (mSelected.get(i, false)) {
-                Cursor cursor = (Cursor) mAdapter.getItem(i);
+            Cursor cursor = (Cursor) mAdapter.getItem(i);
+            long id = cursor.getLong(ID_COLUMN);
+
+            if (mSelected.contains(id)) {
                 String flags = cursor.getString(FLAGS_COLUMN);
 
                 if (!flags.contains(Flag.FLAGGED.name())) {
@@ -2245,8 +2249,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             Set accountUuids = new HashSet(maxAccounts);
 
             for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
-                if (mSelected.get(position, false)) {
-                    Cursor cursor = (Cursor) mAdapter.getItem(position);
+                Cursor cursor = (Cursor) mAdapter.getItem(position);
+                long id = cursor.getLong(ID_COLUMN);
+
+                if (mSelected.contains(id)) {
                     String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
                     accountUuids.add(accountUuid);
 
@@ -2570,7 +2576,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         Message[] messages = new Message[mSelected.size()];
         int out = 0;
         for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
-            if (mSelected.get(position, false)) {
+            Cursor cursor = (Cursor) mAdapter.getItem(position);
+            long id = cursor.getLong(ID_COLUMN);
+
+            if (mSelected.contains(id)) {
                 messages[out++] = getMessageAtPosition(position);
             }
         }
@@ -2775,11 +2784,27 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             cursor = data;
         }
 
-        mSelected = new SparseBooleanArray(cursor.getCount());
-        //TODO: use the (stable) IDs as index and reuse the old mSelected
+        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 id = cursor.getLong(ID_COLUMN);
+            if (mSelected.contains(id)) {
+                selected.add(id);
+            }
+        }
+
+        mSelected = selected;
+    }
+
     @Override
     public void onLoaderReset(Loader loader) {
         mSelected = null;

From 02aeccdedcbcd4790442abef7fea1a4e0c4b2064 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 05:01:25 +0100
Subject: [PATCH 59/74] Make flag operations work on all messages in a thread

---
 .../k9/controller/MessagingController.java    | 49 +++++++++++++++----
 .../fsck/k9/fragment/MessageListFragment.java |  6 ++-
 2 files changed, 44 insertions(+), 11 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 7da30973c..176ea055a 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -2569,7 +2569,28 @@ public class MessagingController implements Runnable {
             }
 
         });
+    }
 
+    public void setFlagForThreads(final Message[] 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);
+                }
+            }
+        });
     }
 
     /**
@@ -3585,16 +3606,7 @@ public class MessagingController implements Runnable {
             List messages) {
 
         try {
-            LocalStore localStore = account.getLocalStore();
-
-            List messagesToDelete = 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(messagesToDelete, messagesInThread);
-            }
+            List messagesToDelete = collectMessagesInThreads(account, messages);
 
             deleteMessagesSynchronous(account, folderName,
                     messagesToDelete.toArray(EMPTY_MESSAGE_ARRAY), null);
@@ -3603,6 +3615,23 @@ public class MessagingController implements Runnable {
         }
     }
 
+    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 Message[] messages, final MessagingListener listener) {
         actOnMessages(messages, new MessageActor() {
 
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index a6f74d9d6..7ccae8599 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1943,7 +1943,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             return;
         }
 
-        mController.setFlag(messages, flag, newState);
+        if (mThreadedList) {
+            mController.setFlagForThreads(messages, flag, newState);
+        } else {
+            mController.setFlag(messages, flag, newState);
+        }
 
         computeBatchDirection();
     }

From 46feb8b9d0557ee6e9141e03bd4be5f29770535d Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 05:24:41 +0100
Subject: [PATCH 60/74] Group messages by account when moving to spam/archive
 folder

---
 .../fsck/k9/fragment/MessageListFragment.java | 52 +++++++++++++------
 1 file changed, 36 insertions(+), 16 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 7ccae8599..4eda679c9 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -6,9 +6,11 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.Date;
 import java.util.EnumMap;
+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;
 
@@ -2021,13 +2023,32 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onArchive(final Message[] messages) {
-        final String folderName = messages[0].getFolder().getAccount().getArchiveFolderName();
-        if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) {
-            return;
+        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().toArray(new Message[0]), archiveFolder);
+            }
         }
-        // TODO one should separate messages by account and call move afterwards
-        // (because each account might have a specific Archive folder name)
-        move(messages, folderName);
+    }
+
+    private Map> groupMessagesByAccount(final Message[] 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) {
@@ -2048,18 +2069,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
     }
 
-    /**
-     * @param holders
-     *            Never {@code null}.
-     */
     private void onSpamConfirmed(Message[] messages) {
-        final String folderName = messages[0].getFolder().getAccount().getSpamFolderName();
-        if (K9.FOLDER_NONE.equalsIgnoreCase(folderName)) {
-            return;
+        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().toArray(new Message[0]), spamFolder);
+            }
         }
-        // TODO one should separate messages by account and call move afterwards
-        // (because each account might have a specific Spam folder name)
-        move(messages, folderName);
     }
 
     private static enum FolderOperation {

From c231b732acc89d1a16689319f43056b988138205 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 05:56:12 +0100
Subject: [PATCH 61/74] Avoid converting lists to arrays when possible

---
 .../k9/controller/MessagingController.java    | 75 +++++++++-------
 .../fsck/k9/fragment/MessageListFragment.java | 85 ++++++++++---------
 .../fsck/k9/fragment/MessageViewFragment.java |  3 +-
 src/com/fsck/k9/provider/MessageProvider.java |  3 +-
 4 files changed, 91 insertions(+), 75 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 176ea055a..3f3ae2f84 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -43,7 +43,6 @@ import com.fsck.k9.R;
 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;
@@ -2556,22 +2555,20 @@ 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 Message[] messages, final Flag flag,
+    public void setFlagForThreads(final List messages, final Flag flag,
             final boolean newState) {
 
         actOnMessages(messages, new MessageActor() {
@@ -2847,14 +2844,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)) {
@@ -3427,40 +3425,52 @@ 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 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 Message[] messages, final String destFolder,
-                             final MessagingListener 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 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 Message[] inMessages,
-            final String destFolder, final boolean isCopy, MessagingListener 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();
@@ -3571,7 +3581,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) {
@@ -3581,7 +3591,7 @@ public class MessagingController implements Runnable {
         }
     }
 
-    public void deleteThreads(final Message[] messages) {
+    public void deleteThreads(final List messages) {
         actOnMessages(messages, new MessageActor() {
 
             @Override
@@ -3632,20 +3642,21 @@ public class MessagingController implements Runnable {
         return messagesInThreads;
     }
 
-    public void deleteMessages(final Message[] messages, final MessagingListener listener) {
+    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 messages) {
-                for (Message message : messages) {
+            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);
                     }
                 });
             }
@@ -4905,7 +4916,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 4eda679c9..b2851f144 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -384,7 +384,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * chosen messages between user interactions (eg. Selecting a folder for
      * move operation)
      */
-    private Message[] mActiveMessages;
+    private List mActiveMessages;
 
     /* package visibility for faster inner class access */
     MessageHelper mMessageHelper;
@@ -1065,10 +1065,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onDelete(Message message) {
-        onDelete(new Message[] { message });
+        onDelete(Collections.singletonList(message));
     }
 
-    private void onDelete(Message[] messages) {
+    private void onDelete(List messages) {
         if (mThreadedList) {
             mController.deleteThreads(messages);
         } else {
@@ -1090,13 +1090,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             }
 
             final String destFolderName = data.getStringExtra(ChooseFolder.EXTRA_NEW_FOLDER);
-            final Message[] messages = mActiveMessages;
+            final List messages = mActiveMessages;
 
             if (destFolderName != null) {
 
                 mActiveMessages = null; // don't need it any more
 
-                final Account account = messages[0].getFolder().getAccount();
+                final Account account = messages.get(0).getFolder().getAccount();
                 account.setLastSelectedFolderName(destFolderName);
 
                 switch (requestCode) {
@@ -1130,7 +1130,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             case R.id.dialog_confirm_spam: {
                 String title = getString(R.string.dialog_confirm_spam_title);
 
-                int selectionSize = mActiveMessages.length;
+                int selectionSize = mActiveMessages.size();
                 String message = getResources().getQuantityString(
                         R.plurals.dialog_confirm_spam_message, selectionSize,
                         Integer.valueOf(selectionSize));
@@ -1937,11 +1937,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void setFlag(Message message, final Flag flag, final boolean newState) {
-        setFlag(new Message[] { message }, flag, newState);
+        setFlag(Collections.singletonList(message), flag, newState);
     }
 
-    private void setFlag(Message[] messages, final Flag flag, final boolean newState) {
-        if (messages.length == 0) {
+    private void setFlag(List messages, final Flag flag, final boolean newState) {
+        if (messages.size() == 0) {
             return;
         }
 
@@ -1955,40 +1955,44 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onMove(Message message) {
-        onMove(new Message[] { message });
+        onMove(Collections.singletonList(message));
     }
 
     /**
      * Display the message move activity.
      *
-     * @param holders
+     * @param messages
      *            Never {@code null}.
      */
-    private void onMove(Message[] messages) {
+    private void onMove(List messages) {
         if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) {
             return;
         }
 
-        final Folder folder = messages.length == 1 ? messages[0].getFolder() : mCurrentFolder.folder;
+        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(new Message[] { message });
+        onCopy(Collections.singletonList(message));
     }
 
     /**
      * Display the message copy activity.
      *
-     * @param holders
+     * @param messages
      *            Never {@code null}.
      */
-    private void onCopy(Message[] messages) {
+    private void onCopy(List messages) {
         if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) {
             return;
         }
 
-        final Folder folder = messages.length == 1 ? messages[0].getFolder() : mCurrentFolder.folder;
+        final Folder folder = (messages.size() == 1) ?
+                messages.get(0).getFolder() : mCurrentFolder.folder;
+
         displayFolderChoice(ACTIVITY_CHOOSE_FOLDER_COPY, folder, messages);
     }
 
@@ -2008,7 +2012,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      *            {@code null}.
      * @see #startActivityForResult(Intent, int)
      */
-    private void displayFolderChoice(final int requestCode, final Folder folder, final Message[] messages) {
+    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());
@@ -2019,10 +2023,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onArchive(final Message message) {
-        onArchive(new Message[] { message });
+        onArchive(Collections.singletonList(message));
     }
 
-    private void onArchive(final Message[] messages) {
+    private void onArchive(final List messages) {
         Map> messagesByAccount = groupMessagesByAccount(messages);
 
         for (Entry> entry : messagesByAccount.entrySet()) {
@@ -2030,12 +2034,12 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             String archiveFolder = account.getArchiveFolderName();
 
             if (!K9.FOLDER_NONE.equals(archiveFolder)) {
-                move(entry.getValue().toArray(new Message[0]), archiveFolder);
+                move(entry.getValue(), archiveFolder);
             }
         }
     }
 
-    private Map> groupMessagesByAccount(final Message[] messages) {
+    private Map> groupMessagesByAccount(final List messages) {
         Map> messagesByAccount = new HashMap>();
         for (Message message : messages) {
             Account account = message.getFolder().getAccount();
@@ -2052,14 +2056,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     private void onSpam(Message message) {
-        onSpam(new Message[] { message });
+        onSpam(Collections.singletonList(message));
     }
 
     /**
      * @param holders
      *            Never {@code null}.
      */
-    private void onSpam(Message[] messages) {
+    private void onSpam(List messages) {
         if (K9.confirmSpam()) {
             // remember the message selection for #onCreateDialog(int)
             mActiveMessages = messages;
@@ -2069,7 +2073,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
     }
 
-    private void onSpamConfirmed(Message[] messages) {
+    private void onSpamConfirmed(List messages) {
         Map> messagesByAccount = groupMessagesByAccount(messages);
 
         for (Entry> entry : messagesByAccount.entrySet()) {
@@ -2077,7 +2081,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             String spamFolder = account.getSpamFolderName();
 
             if (!K9.FOLDER_NONE.equals(spamFolder)) {
-                move(entry.getValue().toArray(new Message[0]), spamFolder);
+                move(entry.getValue(), spamFolder);
             }
         }
     }
@@ -2096,8 +2100,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      *
      * @return true if operation is possible
      */
-    private boolean checkCopyOrMovePossible(final Message[] messages, final FolderOperation operation) {
-        if (messages.length == 0) {
+    private boolean checkCopyOrMovePossible(final List messages,
+            final FolderOperation operation) {
+
+        if (messages.size() == 0) {
             return false;
         }
 
@@ -2130,7 +2136,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * @param holders Never {@code null}.
      * @param destination Never {@code null}.
      */
-    private void copy(Message[] messages, final String destination) {
+    private void copy(List messages, final String destination) {
         copyOrMove(messages, destination, FolderOperation.COPY);
     }
 
@@ -2140,7 +2146,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * @param holders Never {@code null}.
      * @param destination Never {@code null}.
      */
-    private void move(Message[] messages, final String destination) {
+    private void move(List messages, final String destination) {
         copyOrMove(messages, destination, FolderOperation.MOVE);
     }
 
@@ -2158,7 +2164,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * @param operation
      *            Never {@code null}.
      */
-    private void copyOrMove(Message[] messages, final String destination, final FolderOperation operation) {
+    private void copyOrMove(List messages, final String destination, final FolderOperation operation) {
         if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) {
             return;
         }
@@ -2197,11 +2203,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (operation == FolderOperation.MOVE) {
-            mController.moveMessages(account, folderName, outMessages.toArray(new Message[outMessages.size()]), destination,
-                                     null);
+            mController.moveMessages(account, folderName, outMessages, destination, null);
         } else {
-            mController.copyMessages(account, folderName, outMessages.toArray(new Message[outMessages.size()]), destination,
-                                     null);
+            mController.copyMessages(account, folderName, outMessages, destination, null);
         }
     }
 
@@ -2375,7 +2379,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         @Override
         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            Message[] messages = getCheckedMessages();
+            List messages = getCheckedMessages();
 
             /*
              * In the following we assume that we can't move or copy
@@ -2596,15 +2600,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         return null;
     }
 
-    private Message[] getCheckedMessages() {
-        Message[] messages = new Message[mSelected.size()];
-        int out = 0;
+    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 id = cursor.getLong(ID_COLUMN);
 
             if (mSelected.contains(id)) {
-                messages[out++] = getMessageAtPosition(position);
+                messages.add(getMessageAtPosition(position));
             }
         }
 
@@ -2614,7 +2617,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     public void onDelete() {
         Message message = getSelectedMessage();
         if (message != null) {
-            onDelete(new Message[] { message });
+            onDelete(Collections.singletonList(message));
         }
     }
 
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/provider/MessageProvider.java b/src/com/fsck/k9/provider/MessageProvider.java
index 967fe79cf..7035baa65 100644
--- a/src/com/fsck/k9/provider/MessageProvider.java
+++ b/src/com/fsck/k9/provider/MessageProvider.java
@@ -1023,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

From 2cfd25fa23dea701c18404ca67ca39a57e3be9d3 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 06:20:11 +0100
Subject: [PATCH 62/74] Javadoc cleanup

---
 .../fsck/k9/fragment/MessageListFragment.java | 137 ++++++++----------
 1 file changed, 58 insertions(+), 79 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index b2851f144..77a605c8c 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -163,7 +163,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         /**
          * @param delegate
-         *            Never null.
+         *         Never {@code null}.
          */
         public ReverseComparator(final Comparator delegate) {
             mDelegate = delegate;
@@ -186,7 +186,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         /**
          * @param chain
-         *            Comparator chain. Never null.
+         *         Comparator chain. Never {@code null}.
          */
         public ComparatorChain(final List> chain) {
             mChain = chain;
@@ -344,8 +344,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     private Cursor[] mCursors;
 
     /**
-     * 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;
 
@@ -380,9 +379,8 @@ 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;
 
@@ -472,7 +470,7 @@ 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 =
@@ -1385,9 +1383,12 @@ 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];
@@ -1962,7 +1963,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * Display the message move activity.
      *
      * @param messages
-     *            Never {@code null}.
+     *         Never {@code null}.
      */
     private void onMove(List messages) {
         if (!checkCopyOrMovePossible(messages, FolderOperation.MOVE)) {
@@ -1983,7 +1984,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * Display the message copy activity.
      *
      * @param messages
-     *            Never {@code null}.
+     *         Never {@code null}.
      */
     private void onCopy(List messages) {
         if (!checkCopyOrMovePossible(messages, FolderOperation.COPY)) {
@@ -1997,19 +1998,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     /**
-     * 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 messages) {
@@ -2060,8 +2059,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     /**
-     * @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(List messages) {
         if (K9.confirmSpam()) {
@@ -2091,14 +2092,14 @@ 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 messages,
             final FolderOperation operation) {
@@ -2133,8 +2134,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     /**
      * 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(List messages, final String destination) {
         copyOrMove(messages, destination, FolderOperation.COPY);
@@ -2143,8 +2146,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     /**
      * 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(List messages, final String destination) {
         copyOrMove(messages, destination, FolderOperation.MOVE);
@@ -2155,14 +2160,12 @@ 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(List messages, final String destination, final FolderOperation operation) {
         if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) {
@@ -2209,29 +2212,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
     }
 
-    /**
-     * 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.
-     */
-    //TODO: remove
-    /*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;
@@ -2316,42 +2296,41 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         /**
-         * 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
-             */
+        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);
 
             } else {
                 // hide unsupported
-                if (!mController.isCopyCapable(mAccount)) {
+                if (!mController.isCopyCapable(account)) {
                     menu.findItem(R.id.copy).setVisible(false);
                 }
 
-                if (!mController.isMoveCapable(mAccount)) {
+                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 (!mAccount.hasArchiveFolder()) {
+                if (!account.hasArchiveFolder()) {
                     menu.findItem(R.id.archive).setVisible(false);
                 }
 
-                if (!mAccount.hasSpamFolder()) {
+                if (!account.hasSpamFolder()) {
                     menu.findItem(R.id.spam).setVisible(false);
                 }
             }
@@ -2481,8 +2460,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     /**
-     * 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() {

From dfbfaf48dd2cc8dcab4ecbe18d172e063052f8c4 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 06:27:27 +0100
Subject: [PATCH 63/74] Clean up method copyOrMove()

---
 .../fsck/k9/fragment/MessageListFragment.java | 31 +++++++++++++------
 1 file changed, 21 insertions(+), 10 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 77a605c8c..a96eef512 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -2167,7 +2167,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      * @param operation
      *         Specifies what operation to perform. Never {@code null}.
      */
-    private void copyOrMove(List messages, final String destination, final FolderOperation operation) {
+    private void copyOrMove(List messages, final String destination,
+            final FolderOperation operation) {
+
         if (K9.FOLDER_NONE.equalsIgnoreCase(destination)) {
             return;
         }
@@ -2181,27 +2183,36 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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;
             }
+
             outMessages.add(message);
         }
 

From d530b20e2b3e0db1917df9a3f10d70c5cbc57e3f Mon Sep 17 00:00:00 2001
From: cketti 
Date: Sat, 3 Nov 2012 08:30:32 +0100
Subject: [PATCH 64/74] Copy/move all messages in a thread when copying/moving
 placeholder

---
 .../k9/controller/MessagingController.java    | 38 +++++++++++++++++++
 .../fsck/k9/fragment/MessageListFragment.java | 12 +++++-
 2 files changed, 48 insertions(+), 2 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 3f3ae2f84..890d40053 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -3442,6 +3442,27 @@ public class MessagingController implements Runnable {
         });
     }
 
+    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 moveMessage(final Account account, final String srcFolder, final Message message,
             final String destFolder, final MessagingListener listener) {
 
@@ -3461,6 +3482,23 @@ public class MessagingController implements Runnable {
         });
     }
 
+    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);
+                }
+            }
+        });
+    }
+
     public void copyMessage(final Account account, final String srcFolder, final Message message,
             final String destFolder, final MessagingListener listener) {
 
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index a96eef512..f140dd994 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -2217,9 +2217,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (operation == FolderOperation.MOVE) {
-            mController.moveMessages(account, folderName, outMessages, destination, null);
+            if (mThreadedList) {
+                mController.moveMessagesInThread(account, folderName, outMessages, destination);
+            } else {
+                mController.moveMessages(account, folderName, outMessages, destination, null);
+            }
         } else {
-            mController.copyMessages(account, folderName, outMessages, destination, null);
+            if (mThreadedList) {
+                mController.copyMessagesInThread(account, folderName, outMessages, destination);
+            } else {
+                mController.copyMessages(account, folderName, outMessages, destination, null);
+            }
         }
     }
 

From f02d265f604745100f32a202c16a881404bc0bc7 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Mon, 5 Nov 2012 01:06:10 +0100
Subject: [PATCH 65/74] Removed unused column from projection

---
 src/com/fsck/k9/fragment/MessageListFragment.java | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index f140dd994..79e9639e4 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -112,7 +112,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         MessageColumns.FOLDER_ID,
         MessageColumns.PREVIEW,
         MessageColumns.THREAD_ROOT,
-        MessageColumns.THREAD_PARENT,
         SpecialColumns.ACCOUNT_UUID,
         SpecialColumns.FOLDER_NAME,
 
@@ -132,10 +131,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     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 THREAD_PARENT_COLUMN = 13;
-    private static final int ACCOUNT_UUID_COLUMN = 14;
-    private static final int FOLDER_NAME_COLUMN = 15;
-    private static final int THREAD_COUNT_COLUMN = 16;
+    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);

From 52bfea65d1920a88fbeb6038854eb444518bbafa Mon Sep 17 00:00:00 2001
From: cketti 
Date: Mon, 5 Nov 2012 02:11:02 +0100
Subject: [PATCH 66/74] Make the message selection code use the unique (Cursor)
 ID

---
 .../fsck/k9/fragment/MessageListFragment.java | 37 ++++++++++---------
 1 file changed, 20 insertions(+), 17 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 79e9639e4..9cf2d7e62 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -340,6 +340,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     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.
@@ -1630,8 +1631,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             int maybeBoldTypeface = (read) ? Typeface.NORMAL : Typeface.BOLD;
 
-            long id = cursor.getLong(ID_COLUMN);
-            boolean selected = mSelected.contains(id);
+            long uniqueId = cursor.getLong(mUniqueIdColumn);
+            boolean selected = mSelected.contains(uniqueId);
 
             if (selected) {
                 holder.chip.setBackgroundDrawable(account.getCheckmarkChip().drawable());
@@ -1819,8 +1820,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mSelectedCount = 0;
             for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
                 Cursor cursor = (Cursor) mAdapter.getItem(i);
-                long id = cursor.getLong(ID_COLUMN);
-                mSelected.add(id);
+                long uniqueId = cursor.getLong(mUniqueIdColumn);
+                mSelected.add(uniqueId);
 
                 if (mThreadedList) {
                     int threadCount = cursor.getInt(THREAD_COUNT_COLUMN);
@@ -1855,13 +1856,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         Cursor cursor = (Cursor) mAdapter.getItem(adapterPosition);
-        long id = cursor.getLong(ID_COLUMN);
+        long uniqueId = cursor.getLong(mUniqueIdColumn);
 
-        boolean selected = mSelected.contains(id);
+        boolean selected = mSelected.contains(uniqueId);
         if (!selected) {
-            mSelected.add(id);
+            mSelected.add(uniqueId);
         } else {
-            mSelected.remove(id);
+            mSelected.remove(uniqueId);
         }
 
         int selectedCountDelta = 1;
@@ -1913,9 +1914,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         for (int i = 0, end = mAdapter.getCount(); i < end; i++) {
             Cursor cursor = (Cursor) mAdapter.getItem(i);
-            long id = cursor.getLong(ID_COLUMN);
+            long uniqueId = cursor.getLong(mUniqueIdColumn);
 
-            if (mSelected.contains(id)) {
+            if (mSelected.contains(uniqueId)) {
                 String flags = cursor.getString(FLAGS_COLUMN);
 
                 if (!flags.contains(Flag.FLAGGED.name())) {
@@ -2275,9 +2276,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
             for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
                 Cursor cursor = (Cursor) mAdapter.getItem(position);
-                long id = cursor.getLong(ID_COLUMN);
+                long uniqueId = cursor.getLong(mUniqueIdColumn);
 
-                if (mSelected.contains(id)) {
+                if (mSelected.contains(uniqueId)) {
                     String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
                     accountUuids.add(accountUuid);
 
@@ -2600,9 +2601,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         List messages = new ArrayList(mSelected.size());
         for (int position = 0, end = mAdapter.getCount(); position < end; position++) {
             Cursor cursor = (Cursor) mAdapter.getItem(position);
-            long id = cursor.getLong(ID_COLUMN);
+            long uniqueId = cursor.getLong(mUniqueIdColumn);
 
-            if (mSelected.contains(id)) {
+            if (mSelected.contains(uniqueId)) {
                 messages.add(getMessageAtPosition(position));
             }
         }
@@ -2803,8 +2804,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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);
@@ -2819,9 +2822,9 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         Set selected = new HashSet();
         for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) {
-            long id = cursor.getLong(ID_COLUMN);
-            if (mSelected.contains(id)) {
-                selected.add(id);
+            long uniqueId = cursor.getLong(mUniqueIdColumn);
+            if (mSelected.contains(uniqueId)) {
+                selected.add(uniqueId);
             }
         }
 

From ff156ac7c9f404c7df327993cb4151316706f84e Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 14 Nov 2012 00:19:10 +0100
Subject: [PATCH 67/74] Fix remote search

---
 src/com/fsck/k9/activity/MessageList.java     | 40 ++++++------
 .../fsck/k9/fragment/MessageListFragment.java | 63 +++++++++++--------
 src/com/fsck/k9/search/LocalSearch.java       | 12 ++++
 3 files changed, 68 insertions(+), 47 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 0d878801e..2d2795945 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -98,7 +98,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
     private LocalSearch mSearch;
     private boolean mSingleFolderMode;
     private boolean mSingleAccountMode;
-    private boolean mIsRemote;
 
     /**
      * {@code true} if the message list should be displayed as flat list (i.e. no threading)
@@ -128,7 +127,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         if (mMessageListFragment == null) {
             FragmentTransaction ft = fragmentManager.beginTransaction();
             mMessageListFragment = MessageListFragment.newInstance(mSearch,
-                    (K9.isThreadedViewEnabled() && !mNoThreading), mIsRemote);
+                    (K9.isThreadedViewEnabled() && !mNoThreading));
             ft.add(R.id.message_list_container, mMessageListFragment);
             ft.commit();
         }
@@ -142,6 +141,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
                 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));
@@ -152,8 +152,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
                 if (appData != null) {
                     mSearch.addAccountUuid(appData.getString(EXTRA_SEARCH_ACCOUNT));
                     mSearch.addAllowedFolder(appData.getString(EXTRA_SEARCH_FOLDER));
-
-                    mIsRemote = true;
                 } else {
                     mSearch.addAccountUuid(LocalSearch.ALL_ACCOUNTS);
                 }
@@ -479,15 +477,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
                 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.account_settings).setVisible(true);
 
@@ -505,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);
+            }
         }
     }
 
@@ -610,7 +608,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         tmpSearch.addAccountUuids(mSearch.getAccountUuids());
         tmpSearch.and(Searchfield.SENDER, senderAddress, Attribute.CONTAINS);
 
-        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false, false);
+        MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, false);
 
         addMessageListFragment(fragment, true);
     }
@@ -657,14 +655,6 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         }
     }
 
-    @Override
-    public void remoteSearch(String searchAccount, String searchFolder, String queryString) {
-        MessageListFragment fragment = MessageListFragment.newInstance(mSearch, false, true);
-
-        mMenu.findItem(R.id.search_remote).setVisible(false);
-        addMessageListFragment(fragment, false);
-    }
-
     private void addMessageListFragment(MessageListFragment fragment, boolean addToBackStack) {
         FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
 
@@ -700,7 +690,13 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
         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, false);
+        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/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 24504fd90..6f10c754f 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -142,14 +142,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             THREAD_COUNT_COLUMN);
 
 
-    public static MessageListFragment newInstance(LocalSearch search, boolean threadedList,
-            boolean remoteSearch) {
+    public static MessageListFragment newInstance(LocalSearch search, boolean threadedList) {
         MessageListFragment fragment = new MessageListFragment();
         Bundle args = new Bundle();
         args.putParcelable(ARG_SEARCH, search);
         args.putBoolean(ARG_THREADED_LIST, threadedList);
-        //FIXME: Remote search temporarily disabled
-        //args.putBoolean(ARG_REMOTE_SEARCH, remoteSearch);
         fragment.setArguments(args);
         return fragment;
     }
@@ -299,7 +296,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private static final String ARG_SEARCH = "searchObject";
     private static final String ARG_THREADED_LIST = "threadedList";
-    private static final String ARG_REMOTE_SEARCH = "remoteSearch";
     private static final String STATE_LIST_POSITION = "listPosition";
 
     /**
@@ -350,10 +346,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
      */
     private String mFolderName;
 
-    /**
-     * If we're doing a search, this contains the query string.
-     */
-    private boolean mRemoteSearch = false;
+    private boolean mRemoteSearchPerformed = false;
     private Future mRemoteSearchFuture = null;
 
     private String mTitle;
@@ -420,6 +413,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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 folderLoading(String folder, boolean loading) {
@@ -439,6 +433,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
@@ -466,6 +465,10 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                     MessageListFragment.this.progress(progress);
                     break;
                 }
+                case ACTION_REMOTE_SEARCH_FINISHED: {
+                    MessageListFragment.this.remoteSearchFinished();
+                    break;
+                }
             }
         }
     }
@@ -512,7 +515,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private void refreshTitle() {
         setWindowTitle();
-        if (!mRemoteSearch) {
+        if (!mSearch.isManualSearch()) {
             setWindowProgress();
         }
     }
@@ -535,7 +538,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private void setWindowTitle() {
         // regular folder content display
-        if (mSingleFolderMode) {
+        if (!isManualSearch() && mSingleFolderMode) {
             Activity activity = getActivity();
             String displayName = FolderInfoHolder.getDisplayName(activity, mAccount,
                 mFolderName);
@@ -602,7 +605,7 @@ 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) {
+            if (mCurrentFolder != null && !mSearch.isManualSearch()) {
                 mController.loadMoreMessages(mAccount, mFolderName, null);
             } /*else if (mRemoteSearch && mAdapter.mExtraSearchResults != null && mAdapter.mExtraSearchResults.size() > 0 && mSearchAccount != null) {
                 int numResults = mAdapter.mExtraSearchResults.size();
@@ -728,7 +731,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         Bundle args = getArguments();
 
         mThreadedList = args.getBoolean(ARG_THREADED_LIST, false);
-        mRemoteSearch = args.getBoolean(ARG_REMOTE_SEARCH, false);
         mSearch = args.getParcelable(ARG_SEARCH);
         mTitle = mSearch.getName();
 
@@ -881,7 +883,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
 
         if (mSingleFolderMode) {
-            if (mRemoteSearch && mAccount.allowRemoteSearch()) {
+            if (mSearch.isManualSearch() && mAccount.allowRemoteSearch()) {
                 mPullToRefreshView.setOnRefreshListener(new PullToRefreshBase.OnRefreshListener() {
                     @Override
                     public void onRefresh(PullToRefreshBase refreshView) {
@@ -919,7 +921,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
             mController.notifyAccountCancel(appContext, accountWithNotification);
         }
 
-        if (mAccount != null && mFolderName != null && !mRemoteSearch) {
+        if (mAccount != null && mFolderName != null && !mSearch.isManualSearch()) {
             mController.getFolderUnreadMessageCount(mAccount, mFolderName, mListener);
         }
 
@@ -984,7 +986,13 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         searchAccount = mAccount.getUuid();
         searchFolder = mCurrentFolder.name;
 
-        mFragmentListener.remoteSearch(searchAccount, searchFolder, mSearch.getRemoteSearchArguments());
+        String queryString = mSearch.getRemoteSearchArguments();
+
+        mRemoteSearchPerformed = true;
+        mRemoteSearchFuture = mController.searchRemoteMessages(searchAccount, searchFolder,
+                queryString, null, null, mListener);
+
+        mFragmentListener.remoteSearchStarted();
     }
 
     /**
@@ -1435,6 +1443,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         @Override
         public void remoteSearchFinished(Account acct, String folder, int numResults, List extraResults) {
             mHandler.progress(false);
+            mHandler.remoteSearchFinished();
             if (extraResults != null && extraResults.size() > 0) {
                 mHandler.updateFooter(String.format(mContext.getString(R.string.load_more_messages_fmt), acct.getRemoteSearchNumResults()), false);
             } else {
@@ -1790,7 +1799,7 @@ 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(mContext.getString(R.string.status_loading_more), showProgress);
@@ -2589,7 +2598,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     public interface MessageListFragmentListener {
         void setMessageListProgress(int level);
         void showThread(Account account, String folderName, long rootId);
-        void remoteSearch(String searchAccount, String searchFolder, String queryString);
         void showMoreFromSameSender(String senderAddress);
         void onResendMessage(Message message);
         void onForward(Message message);
@@ -2601,6 +2609,7 @@ 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() {
@@ -2695,10 +2704,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         }
     }
 
-    public boolean isSearchQuery() {
-        return (mSearch.getRemoteSearchArguments() != null || !mSingleAccountMode);
-    }
-
     public boolean isOutbox() {
         return (mFolderName != null && mFolderName.equals(mAccount.getOutboxFolderName()));
     }
@@ -2708,7 +2713,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     public boolean isRemoteFolder() {
-        if (isSearchQuery() || isOutbox() || isErrorFolder()) {
+        if (mSearch.isManualSearch() || isOutbox() || isErrorFolder()) {
             return false;
         }
 
@@ -2720,6 +2725,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());
@@ -2739,11 +2748,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     }
 
     public boolean isRemoteSearch() {
-        return mRemoteSearch;
+        return mRemoteSearchPerformed;
     }
 
     public boolean isRemoteSearchAllowed() {
-        if (!isSearchQuery() || mRemoteSearch || !mSingleFolderMode) {
+        if (!mSearch.isManualSearch() || mRemoteSearchPerformed || !mSingleFolderMode) {
             return false;
         }
 
@@ -2879,4 +2888,8 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         String accountUuid = cursor.getString(ACCOUNT_UUID_COLUMN);
         return mPreferences.getAccount(accountUuid);
     }
+
+    private void remoteSearchFinished() {
+        mRemoteSearchFuture = null;
+    }
 }
diff --git a/src/com/fsck/k9/search/LocalSearch.java b/src/com/fsck/k9/search/LocalSearch.java
index ac3463258..a6041c41f 100644
--- a/src/com/fsck/k9/search/LocalSearch.java
+++ b/src/com/fsck/k9/search/LocalSearch.java
@@ -27,6 +27,7 @@ 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();
@@ -85,6 +86,7 @@ public class LocalSearch implements SearchSpecification {
         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;
@@ -340,6 +342,14 @@ public class LocalSearch implements SearchSpecification {
         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.
@@ -388,6 +398,7 @@ public class LocalSearch implements SearchSpecification {
     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);
     }
@@ -409,6 +420,7 @@ public class LocalSearch implements SearchSpecification {
     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();

From 9cbbd063526965813704a1d892afd77c7b60c293 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 14 Nov 2012 02:08:08 +0100
Subject: [PATCH 68/74] Restore settings to configure remote search

---
 .../fsck/k9/activity/setup/AccountSettings.java  | 16 +---------------
 1 file changed, 1 insertion(+), 15 deletions(-)

diff --git a/src/com/fsck/k9/activity/setup/AccountSettings.java b/src/com/fsck/k9/activity/setup/AccountSettings.java
index 302cf1528..8c29fc2e8 100644
--- a/src/com/fsck/k9/activity/setup/AccountSettings.java
+++ b/src/com/fsck/k9/activity/setup/AccountSettings.java
@@ -180,12 +180,9 @@ public class AccountSettings extends K9PreferenceActivity {
     private CheckBoxPreference mCryptoAutoEncrypt;
 
     private PreferenceScreen mSearchScreen;
-    //FIXME: Remote search is temporarily disabled
-    /*
     private CheckBoxPreference mCloudSearchEnabled;
     private ListPreference mRemoteSearchNumResults;
     private CheckBoxPreference mRemoteSearchFullText;
-    */
 
     private ListPreference mLocalStorageProvider;
     private ListPreference mArchiveFolder;
@@ -503,9 +500,6 @@ public class AccountSettings extends K9PreferenceActivity {
 
         mSearchScreen = (PreferenceScreen) findPreference(PREFERENCE_SCREEN_SEARCH);
 
-        //FIXME: Remote search is temporarily disabled
-        mMainScreen.removePreference(mSearchScreen);
-        /*
         mCloudSearchEnabled = (CheckBoxPreference) findPreference(PREFERENCE_CLOUD_SEARCH_ENABLED);
         mRemoteSearchNumResults = (ListPreference) findPreference(PREFERENCE_REMOTE_SEARCH_NUM_RESULTS);
         mRemoteSearchNumResults.setOnPreferenceChangeListener(
@@ -518,7 +512,6 @@ public class AccountSettings extends K9PreferenceActivity {
         );
         updateRemoteSearchLimit(mRemoteSearchNumResults.getValue());
         mRemoteSearchFullText = (CheckBoxPreference) findPreference(PREFERENCE_REMOTE_SEARCH_FULL_TEXT);
-        */
 
         mPushPollOnConnect = (CheckBoxPreference) findPreference(PREFERENCE_PUSH_POLL_ON_CONNECT);
         mIdleRefreshPeriod = (ListPreference) findPreference(PREFERENCE_IDLE_REFRESH_PERIOD);
@@ -526,12 +519,9 @@ public class AccountSettings extends K9PreferenceActivity {
         if (mIsPushCapable) {
             mPushPollOnConnect.setChecked(mAccount.isPushPollOnConnect());
 
-            //FIXME: Remote search is temporarily disabled
-            /*
             mCloudSearchEnabled.setChecked(mAccount.allowRemoteSearch());
             mRemoteSearchNumResults.setValue(Integer.toString(mAccount.getRemoteSearchNumResults()));
             mRemoteSearchFullText.setChecked(mAccount.isRemoteSearchFullText());
-            */
 
             mIdleRefreshPeriod.setValue(String.valueOf(mAccount.getIdleRefreshMinutes()));
             mIdleRefreshPeriod.setSummary(mIdleRefreshPeriod.getEntry());
@@ -805,12 +795,9 @@ public class AccountSettings extends K9PreferenceActivity {
             mAccount.setPushPollOnConnect(mPushPollOnConnect.isChecked());
             mAccount.setIdleRefreshMinutes(Integer.parseInt(mIdleRefreshPeriod.getValue()));
             mAccount.setMaxPushFolders(Integer.parseInt(mMaxPushFolders.getValue()));
-            //FIXME: Remote search is temporarily disabled
-            /*
             mAccount.setAllowRemoteSearch(mCloudSearchEnabled.isChecked());
             mAccount.setRemoteSearchNumResults(Integer.parseInt(mRemoteSearchNumResults.getValue()));
             mAccount.setRemoteSearchFullText(mRemoteSearchFullText.isChecked());
-            */
         }
 
         if (!mIsMoveCapable) {
@@ -995,8 +982,7 @@ public class AccountSettings extends K9PreferenceActivity {
                 maxResults = getString(R.string.account_settings_remote_search_num_results_entries_all);
             }
 
-            //FIXME: Remote search is temporarily disabled
-            //mRemoteSearchNumResults.setSummary(String.format(getString(R.string.account_settings_remote_search_num_summary), maxResults));
+            mRemoteSearchNumResults.setSummary(String.format(getString(R.string.account_settings_remote_search_num_summary), maxResults));
         }
     }
 

From 38fe4d199003b944249efdc5d1fa73d675ebc3d4 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Wed, 14 Nov 2012 15:01:18 +0100
Subject: [PATCH 69/74] Fix "up" navigation when leaving a message list with
 search results

---
 src/com/fsck/k9/activity/MessageList.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index 2d2795945..fcbb78bd2 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -345,7 +345,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
                 FragmentManager fragmentManager = getSupportFragmentManager();
                 if (fragmentManager.getBackStackEntryCount() > 0) {
                     fragmentManager.popBackStack();
-                } else if (!mSingleFolderMode) {
+                } else if (!mSingleFolderMode || mMessageListFragment.isManualSearch()) {
                     onBackPressed();
                 } else {
                     onShowFolderList();

From b108e7a539d2ea638349a77ffe8cd8aee001e6c2 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 15 Nov 2012 21:05:45 +0100
Subject: [PATCH 70/74] Don't fetch already downloaded messages found by a
 server-side search

---
 .../k9/controller/MessagingController.java    | 24 +++---
 src/com/fsck/k9/mail/store/LocalStore.java    | 81 +++++++++++++++++++
 2 files changed, 95 insertions(+), 10 deletions(-)

diff --git a/src/com/fsck/k9/controller/MessagingController.java b/src/com/fsck/k9/controller/MessagingController.java
index 890d40053..d23d5799c 100644
--- a/src/com/fsck/k9/controller/MessagingController.java
+++ b/src/com/fsck/k9/controller/MessagingController.java
@@ -626,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) {
diff --git a/src/com/fsck/k9/mail/store/LocalStore.java b/src/com/fsck/k9/mail/store/LocalStore.java
index f4120408a..21f03472f 100644
--- a/src/com/fsck/k9/mail/store/LocalStore.java
+++ b/src/com/fsck/k9/mail/store/LocalStore.java
@@ -15,6 +15,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Date;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Locale;
@@ -101,6 +102,14 @@ public class LocalStore extends Store implements Serializable {
 
     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" };
+
+    /**
+     * 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;
 
@@ -3206,6 +3215,78 @@ public class LocalStore extends Store implements Serializable {
 
             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 {

From 5accadab00084bbd004aadcfda82c61a38969757 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 15 Nov 2012 21:26:46 +0100
Subject: [PATCH 71/74] Enable loading more search results from the server

---
 .../fsck/k9/fragment/MessageListFragment.java | 29 +++++++++++--------
 1 file changed, 17 insertions(+), 12 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 6f10c754f..65df35f30 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -348,6 +348,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
     private boolean mRemoteSearchPerformed = false;
     private Future mRemoteSearchFuture = null;
+    public List mExtraSearchResults;
 
     private String mTitle;
     private LocalSearch mSearch = null;
@@ -606,26 +607,29 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     public void onItemClick(AdapterView parent, View view, int position, long id) {
         if (view == mFooterView) {
             if (mCurrentFolder != null && !mSearch.isManualSearch()) {
+
                 mController.loadMoreMessages(mAccount, mFolderName, null);
-            } /*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) {
-                    updateFooter("", false);
-                    return;
-                }
+
+            } else if (mCurrentFolder != null && isRemoteSearch() &&
+                    mExtraSearchResults != null && mExtraSearchResults.size() > 0) {
+
+                int numResults = mExtraSearchResults.size();
                 int limit = mAccount.getRemoteSearchNumResults();
-                List toProcess = mAdapter.mExtraSearchResults;
+
+                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;
+                    mExtraSearchResults = null;
                     updateFooter("", false);
                 }
+
                 mController.loadSearchResults(mAccount, mCurrentFolder.name, toProcess, mListener);
-            }*/
+            }
+
             return;
         }
 
@@ -1444,6 +1448,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
         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 {

From 4eefcb3a311edf9037b7ffe29b25c4322d317d18 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 15 Nov 2012 21:28:05 +0100
Subject: [PATCH 72/74] Fix crash when remote search fails after search screen
 has been left

Also, change error message to be less technical.
---
 res/values/strings.xml                            | 1 +
 src/com/fsck/k9/fragment/MessageListFragment.java | 7 +++++--
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/res/values/strings.xml b/res/values/strings.xml
index 3bf56d49c..84fc992d7 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1139,6 +1139,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
diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index 65df35f30..fa520dc08 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -1428,11 +1428,14 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
     class MessageListActivityListener extends ActivityListener {
         @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();
+                    Activity activity = getActivity();
+                    if (activity != null) {
+                        Toast.makeText(activity, R.string.remote_search_error,
+                                Toast.LENGTH_LONG).show();
+                    }
                 }
             });
         }

From 999dd3316913c4d1a29cd7b908a16720369472f0 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 15 Nov 2012 21:42:10 +0100
Subject: [PATCH 73/74] Prevent NPEs in methods called by the handler after
 activity detached

---
 .../fsck/k9/fragment/MessageListFragment.java  | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/src/com/fsck/k9/fragment/MessageListFragment.java b/src/com/fsck/k9/fragment/MessageListFragment.java
index fa520dc08..d5c38e193 100644
--- a/src/com/fsck/k9/fragment/MessageListFragment.java
+++ b/src/com/fsck/k9/fragment/MessageListFragment.java
@@ -450,6 +450,20 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
 
         @Override
         public void handleMessage(android.os.Message msg) {
+            // The following messages don't need an attached activity.
+            switch (msg.what) {
+                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;
@@ -466,10 +480,6 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
                     MessageListFragment.this.progress(progress);
                     break;
                 }
-                case ACTION_REMOTE_SEARCH_FINISHED: {
-                    MessageListFragment.this.remoteSearchFinished();
-                    break;
-                }
             }
         }
     }

From 411abbea6411e92435113182d60fa0397968e425 Mon Sep 17 00:00:00 2001
From: cketti 
Date: Thu, 15 Nov 2012 21:45:36 +0100
Subject: [PATCH 74/74] Fix display of folder settings and account settings
 menu entries

---
 src/com/fsck/k9/activity/MessageList.java | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/com/fsck/k9/activity/MessageList.java b/src/com/fsck/k9/activity/MessageList.java
index fcbb78bd2..c5ad31281 100644
--- a/src/com/fsck/k9/activity/MessageList.java
+++ b/src/com/fsck/k9/activity/MessageList.java
@@ -396,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;
@@ -423,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;
@@ -478,7 +478,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
                 menu.findItem(R.id.folder_settings).setVisible(false);
                 menu.findItem(R.id.account_settings).setVisible(false);
             } else {
-                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()) {