From 95b39c71d2539061ef2c93669a228e1b70f8a1a7 Mon Sep 17 00:00:00 2001 From: cketti Date: Tue, 23 Oct 2012 03:01:50 +0200 Subject: [PATCH] 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;