Add threading support to content provider
This commit is contained in:
parent
05a2571570
commit
95b39c71d2
5 changed files with 161 additions and 38 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Cursor> {
|
||||
|
||||
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<String> queryArgs = new ArrayList<String>();
|
||||
|
@ -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");
|
||||
}
|
||||
|
||||
|
|
|
@ -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<String> 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<Cursor>() {
|
||||
@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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
Loading…
Reference in a new issue