1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-11-24 02:12:15 -05:00

Use separate table to store the thread structure

This commit is contained in:
cketti 2013-01-11 03:40:35 +01:00
parent 3f84bb54f2
commit 1df88ea153
7 changed files with 617 additions and 291 deletions

View File

@ -711,8 +711,7 @@ public class MessageList extends K9FragmentActivity implements MessageListFragme
public void showThread(Account account, String folderName, long threadRootId) { public void showThread(Account account, String folderName, long threadRootId) {
LocalSearch tmpSearch = new LocalSearch(); LocalSearch tmpSearch = new LocalSearch();
tmpSearch.addAccountUuid(account.getUuid()); tmpSearch.addAccountUuid(account.getUuid());
tmpSearch.and(Searchfield.THREAD_ROOT, String.valueOf(threadRootId), Attribute.EQUALS); tmpSearch.and(Searchfield.THREAD_ID, String.valueOf(threadRootId), Attribute.EQUALS);
tmpSearch.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, String.valueOf(threadRootId)));
MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, true, false); MessageListFragment fragment = MessageListFragment.newInstance(tmpSearch, true, false);
addMessageListFragment(fragment, true); addMessageListFragment(fragment, true);

View File

@ -3758,8 +3758,9 @@ public class MessagingController implements Runnable {
List<Message> messagesInThreads = new ArrayList<Message>(); List<Message> messagesInThreads = new ArrayList<Message>();
for (Message message : messages) { for (Message message : messages) {
long rootId = ((LocalMessage) message).getRootId(); LocalMessage localMessage = (LocalMessage) message;
long threadId = (rootId == -1) ? message.getId() : rootId; long rootId = localMessage.getRootId();
long threadId = (rootId == -1) ? localMessage.getThreadId() : rootId;
Message[] messagesInThread = localStore.getMessagesInThread(threadId); Message[] messagesInThread = localStore.getMessagesInThread(threadId);
Collections.addAll(messagesInThreads, messagesInThread); Collections.addAll(messagesInThreads, messagesInThread);

View File

@ -91,8 +91,12 @@ import com.fsck.k9.mail.store.LocalStore.LocalFolder;
import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.provider.EmailProvider.MessageColumns; import com.fsck.k9.provider.EmailProvider.MessageColumns;
import com.fsck.k9.provider.EmailProvider.SpecialColumns; import com.fsck.k9.provider.EmailProvider.SpecialColumns;
import com.fsck.k9.provider.EmailProvider.ThreadColumns;
import com.fsck.k9.search.ConditionsTreeNode;
import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification; import com.fsck.k9.search.SearchSpecification;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
import com.fsck.k9.search.SearchSpecification.Searchfield;
import com.fsck.k9.search.SqlQueryBuilder; import com.fsck.k9.search.SqlQueryBuilder;
import com.handmark.pulltorefresh.library.PullToRefreshBase; import com.handmark.pulltorefresh.library.PullToRefreshBase;
import com.handmark.pulltorefresh.library.PullToRefreshListView; import com.handmark.pulltorefresh.library.PullToRefreshListView;
@ -117,11 +121,11 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
MessageColumns.ATTACHMENT_COUNT, MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID, MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW, MessageColumns.PREVIEW,
MessageColumns.THREAD_ROOT, ThreadColumns.ROOT,
SpecialColumns.ACCOUNT_UUID, SpecialColumns.ACCOUNT_UUID,
SpecialColumns.FOLDER_NAME, SpecialColumns.FOLDER_NAME,
MessageColumns.THREAD_COUNT, SpecialColumns.THREAD_COUNT,
}; };
private static final int ID_COLUMN = 0; private static final int ID_COLUMN = 0;
@ -2083,15 +2087,7 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
accountMapping.put(account, messageIdList); accountMapping.put(account, messageIdList);
} }
long selectionId; messageIdList.add(cursor.getLong(ID_COLUMN));
if (mThreadedList) {
selectionId = (cursor.isNull(THREAD_ROOT_COLUMN)) ?
cursor.getLong(ID_COLUMN) : cursor.getLong(THREAD_ROOT_COLUMN);
} else {
selectionId = cursor.getLong(ID_COLUMN);
}
messageIdList.add(selectionId);
} }
} }
@ -2888,19 +2884,30 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
String accountUuid = mAccountUuids[id]; String accountUuid = mAccountUuids[id];
Account account = mPreferences.getAccount(accountUuid); Account account = mPreferences.getAccount(accountUuid);
String threadId = getThreadId(mSearch);
Uri uri; Uri uri;
String[] projection; String[] projection;
if (mThreadedList) { boolean needConditions;
if (threadId != null) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/thread/" + threadId);
projection = PROJECTION;
needConditions = false;
} else if (mThreadedList) {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded"); uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages/threaded");
projection = THREADED_PROJECTION; projection = THREADED_PROJECTION;
needConditions = true;
} else { } else {
uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages"); uri = Uri.withAppendedPath(EmailProvider.CONTENT_URI, "account/" + accountUuid + "/messages");
projection = PROJECTION; projection = PROJECTION;
needConditions = true;
} }
StringBuilder query = new StringBuilder(); StringBuilder query = new StringBuilder();
List<String> queryArgs = new ArrayList<String>(); List<String> queryArgs = new ArrayList<String>();
if (needConditions) {
SqlQueryBuilder.buildWhereClause(account, mSearch.getConditions(), query, queryArgs); SqlQueryBuilder.buildWhereClause(account, mSearch.getConditions(), query, queryArgs);
}
String selection = query.toString(); String selection = query.toString();
String[] selectionArgs = queryArgs.toArray(new String[0]); String[] selectionArgs = queryArgs.toArray(new String[0]);
@ -2911,6 +2918,17 @@ public class MessageListFragment extends SherlockFragment implements OnItemClick
sortOrder); sortOrder);
} }
private String getThreadId(LocalSearch search) {
for (ConditionsTreeNode node : search.getLeafSet()) {
SearchCondition condition = node.mCondition;
if (condition.field == Searchfield.THREAD_ID) {
return condition.value;
}
}
return null;
}
private String buildSortOrder() { private String buildSortOrder() {
String sortColumn = MessageColumns.ID; String sortColumn = MessageColumns.ID;
switch (mSortType) { switch (mSortType) {

View File

@ -74,7 +74,6 @@ import com.fsck.k9.provider.AttachmentProvider;
import com.fsck.k9.provider.EmailProvider; import com.fsck.k9.provider.EmailProvider;
import com.fsck.k9.search.LocalSearch; import com.fsck.k9.search.LocalSearch;
import com.fsck.k9.search.SearchSpecification.Attribute; 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.SearchSpecification.Searchfield;
import com.fsck.k9.search.SqlQueryBuilder; import com.fsck.k9.search.SqlQueryBuilder;
@ -96,8 +95,10 @@ public class LocalStore extends Store implements Serializable {
* in the correct order. * in the correct order.
*/ */
static private String GET_MESSAGES_COLS = static private String GET_MESSAGES_COLS =
"subject, sender_list, date, uid, flags, messages.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, deleted, read, flagged, answered, forwarded "; "bcc_list, reply_to_list, attachment_count, internal_date, messages.message_id, " +
"folder_id, preview, threads.id, threads.root, deleted, read, flagged, answered, " +
"forwarded ";
static private String GET_FOLDER_COLS = "folders.id, name, SUM(read=0), visible_limit, last_updated, status, push_state, last_pushed, SUM(flagged), integrate, top_group, poll_class, push_class, display_class"; static private String GET_FOLDER_COLS = "folders.id, name, SUM(read=0), visible_limit, last_updated, status, push_state, last_pushed, SUM(flagged), integrate, top_group, poll_class, push_class, display_class";
@ -117,7 +118,7 @@ public class LocalStore extends Store implements Serializable {
*/ */
private static final int FLAG_UPDATE_BATCH_SIZE = 500; private static final int FLAG_UPDATE_BATCH_SIZE = 500;
public static final int DB_VERSION = 46; public static final int DB_VERSION = 47;
protected String uUid = null; protected String uUid = null;
@ -204,8 +205,6 @@ public class LocalStore extends Store implements Serializable {
"message_id TEXT, " + "message_id TEXT, " +
"preview TEXT, " + "preview TEXT, " +
"mime_type TEXT, "+ "mime_type TEXT, "+
"thread_root INTEGER, " +
"thread_parent INTEGER, " +
"normalized_subject_hash INTEGER, " + "normalized_subject_hash INTEGER, " +
"empty INTEGER, " + "empty INTEGER, " +
"read INTEGER default 0, " + "read INTEGER default 0, " +
@ -226,18 +225,30 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("DROP INDEX IF EXISTS msg_empty"); db.execSQL("DROP INDEX IF EXISTS msg_empty");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_empty ON messages (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 INDEX IF EXISTS msg_read"); db.execSQL("DROP INDEX IF EXISTS msg_read");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
db.execSQL("DROP INDEX IF EXISTS msg_flagged"); db.execSQL("DROP INDEX IF EXISTS msg_flagged");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
db.execSQL("DROP TABLE IF EXISTS threads");
db.execSQL("CREATE TABLE threads (" +
"id INTEGER PRIMARY KEY, " +
"message_id INTEGER, " +
"root INTEGER, " +
"parent INTEGER" +
")");
db.execSQL("DROP INDEX IF EXISTS threads_message_id");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
db.execSQL("DROP INDEX IF EXISTS threads_root");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
db.execSQL("DROP INDEX IF EXISTS threads_parent");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
db.execSQL("DROP TABLE IF EXISTS attachments"); db.execSQL("DROP TABLE IF EXISTS attachments");
db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER," db.execSQL("CREATE TABLE attachments (id INTEGER PRIMARY KEY, message_id INTEGER,"
+ "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT," + "store_data TEXT, content_uri TEXT, size INTEGER, name TEXT,"
@ -539,6 +550,76 @@ public class LocalStore extends Store implements Serializable {
db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_read ON messages (read)");
db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)"); db.execSQL("CREATE INDEX IF NOT EXISTS msg_flagged ON messages (flagged)");
} }
if (db.getVersion() < 47) {
// Create new 'threads' table
db.execSQL("DROP TABLE IF EXISTS threads");
db.execSQL("CREATE TABLE threads (" +
"id INTEGER PRIMARY KEY, " +
"message_id INTEGER, " +
"root INTEGER, " +
"parent INTEGER" +
")");
// Create indices for new table
db.execSQL("DROP INDEX IF EXISTS threads_message_id");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_message_id ON threads (message_id)");
db.execSQL("DROP INDEX IF EXISTS threads_root");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_root ON threads (root)");
db.execSQL("DROP INDEX IF EXISTS threads_parent");
db.execSQL("CREATE INDEX IF NOT EXISTS threads_parent ON threads (parent)");
// Create entries for all messages in 'threads' table
db.execSQL("INSERT INTO threads (message_id) SELECT id FROM messages");
// Copy thread structure from 'messages' table to 'threads'
Cursor cursor = db.query("messages",
new String[] { "id", "thread_root", "thread_parent" },
null, null, null, null, null);
try {
ContentValues cv = new ContentValues();
while (cursor.moveToNext()) {
cv.clear();
long messageId = cursor.getLong(0);
if (!cursor.isNull(1)) {
long threadRootMessageId = cursor.getLong(1);
db.execSQL("UPDATE threads SET root = (SELECT t.id FROM " +
"threads t WHERE t.message_id = ?) " +
"WHERE message_id = ?",
new String[] {
Long.toString(threadRootMessageId),
Long.toString(messageId)
});
}
if (!cursor.isNull(2)) {
long threadParentMessageId = cursor.getLong(2);
db.execSQL("UPDATE threads SET parent = (SELECT t.id FROM " +
"threads t WHERE t.message_id = ?) " +
"WHERE message_id = ?",
new String[] {
Long.toString(threadParentMessageId),
Long.toString(messageId)
});
}
}
} finally {
cursor.close();
}
// Remove indices for old thread-related columns in 'messages' table
db.execSQL("DROP INDEX IF EXISTS msg_thread_root");
db.execSQL("DROP INDEX IF EXISTS msg_thread_parent");
// Clear out old thread-related columns in 'messages'
ContentValues cv = new ContentValues();
cv.putNull("thread_root");
cv.putNull("thread_parent");
db.update("messages", cv, null, null);
}
} }
} catch (SQLiteException e) { } catch (SQLiteException e) {
Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0"); Log.e(K9.LOG_TAG, "Exception while upgrading database. Resetting the DB to v0");
@ -671,8 +752,21 @@ public class LocalStore extends Store implements Serializable {
database.execute(false, new DbCallback<Void>() { database.execute(false, new DbCallback<Void>() {
@Override @Override
public Void doDbWork(final SQLiteDatabase db) { public Void doDbWork(final SQLiteDatabase db) {
db.execSQL("DELETE FROM messages WHERE deleted = 0 and uid not like 'Local%'"); // Delete entries from 'threads' table
db.execSQL("update folders set flagged_count = 0, unread_count = 0"); db.execSQL("DELETE FROM threads WHERE message_id IN " +
"(SELECT id FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%')");
// Set 'root' and 'parent' of remaining entries in 'thread' table to 'NULL' to make
// sure the thread structure is in a valid state (this may destroy existing valid
// thread trees, but is much faster than adjusting the tree by removing messages
// one by one).
ContentValues cv = new ContentValues();
cv.putNull("root");
cv.putNull("parent");
db.update("threads", cv, null, null);
// Delete entries from 'messages' table
db.execSQL("DELETE FROM messages WHERE deleted = 0 AND uid NOT LIKE 'Local%'");
return null; return null;
} }
}); });
@ -979,6 +1073,7 @@ public class LocalStore extends Store implements Serializable {
String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY); String[] selectionArgs = queryArgs.toArray(EMPTY_STRING_ARRAY);
String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " + String sqlQuery = "SELECT " + GET_MESSAGES_COLS + "FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " + "LEFT JOIN folders ON (folders.id = messages.folder_id) WHERE " +
"((empty IS NULL OR empty != 1) AND deleted = 0)" + "((empty IS NULL OR empty != 1) AND deleted = 0)" +
((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") + ((!StringUtils.isNullOrEmpty(where)) ? " AND (" + where + ")" : "") +
@ -1052,8 +1147,7 @@ public class LocalStore extends Store implements Serializable {
String rootIdString = Long.toString(rootId); String rootIdString = Long.toString(rootId);
LocalSearch search = new LocalSearch(); LocalSearch search = new LocalSearch();
search.and(Searchfield.THREAD_ROOT, rootIdString, Attribute.EQUALS); search.and(Searchfield.THREAD_ID, rootIdString, Attribute.EQUALS);
search.or(new SearchCondition(Searchfield.ID, Attribute.EQUALS, rootIdString));
return searchForMessages(null, search); return searchForMessages(null, search);
} }
@ -1947,9 +2041,11 @@ public class LocalStore extends Store implements Serializable {
try { try {
cursor = db.rawQuery( cursor = db.rawQuery(
"SELECT " "SELECT " +
+ GET_MESSAGES_COLS GET_MESSAGES_COLS +
+ "FROM messages WHERE uid = ? AND folder_id = ?", "FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"WHERE uid = ? AND folder_id = ?",
new String[] { new String[] {
message.getUid(), Long.toString(mFolderId) message.getUid(), Long.toString(mFolderId)
}); });
@ -1987,13 +2083,13 @@ public class LocalStore extends Store implements Serializable {
return LocalStore.this.getMessages( return LocalStore.this.getMessages(
listener, listener,
LocalFolder.this, LocalFolder.this,
"SELECT " + GET_MESSAGES_COLS "SELECT " + GET_MESSAGES_COLS +
+ "FROM messages WHERE (empty IS NULL OR empty != 1) AND " "FROM messages " +
+ (includeDeleted ? "" : "deleted = 0 AND ") "JOIN threads ON (threads.message_id = messages.id) " +
+ " folder_id = ? ORDER BY date DESC" "WHERE (empty IS NULL OR empty != 1) AND " +
, new String[] { (includeDeleted ? "" : "deleted = 0 AND ") +
Long.toString(mFolderId) "folder_id = ? ORDER BY date DESC",
} new String[] { Long.toString(mFolderId) }
); );
} catch (MessagingException e) { } catch (MessagingException e) {
throw new WrappedException(e); throw new WrappedException(e);
@ -2065,36 +2161,34 @@ public class LocalStore extends Store implements Serializable {
ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message); ThreadInfo threadInfo = lDestFolder.doMessageThreading(db, message);
/* /*
* "Move" the message into the new folder and thread structure * "Move" the message into the new folder
*/ */
long id = lMessage.getId(); long msgId = lMessage.getId();
String[] idArg = new String[] { Long.toString(id) }; String[] idArg = new String[] { Long.toString(msgId) };
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put("folder_id", lDestFolder.getId()); cv.put("folder_id", lDestFolder.getId());
cv.put("uid", newUid); cv.put("uid", newUid);
db.update("messages", cv, "id = ?", idArg);
// Create/update entry in 'threads' table for the message in the
// target folder
cv.clear();
cv.put("message_id", msgId);
if (threadInfo.threadId == -1) {
if (threadInfo.rootId != -1) { if (threadInfo.rootId != -1) {
cv.put("thread_root", threadInfo.rootId); cv.put("root", threadInfo.rootId);
} }
if (threadInfo.parentId != -1) { if (threadInfo.parentId != -1) {
cv.put("thread_parent", threadInfo.parentId); cv.put("parent", threadInfo.parentId);
} }
db.update("messages", cv, "id = ?", idArg); db.insert("threads", null, cv);
} else {
if (threadInfo.id != -1) { db.update("threads", cv, "id = ?",
String[] oldIdArg = new String[] { Long.toString(threadInfo.threadId) });
new String[] { Long.toString(threadInfo.id) };
cv.clear();
cv.put("thread_root", id);
db.update("messages", cv, "thread_root = ?", oldIdArg);
cv.clear();
cv.put("thread_parent", id);
db.update("messages", cv, "thread_parent = ?", oldIdArg);
} }
/* /*
@ -2112,29 +2206,19 @@ public class LocalStore extends Store implements Serializable {
cv.put("read", 1); cv.put("read", 1);
cv.put("deleted", 1); cv.put("deleted", 1);
cv.put("folder_id", mFolderId); cv.put("folder_id", mFolderId);
cv.put("empty", 0);
String messageId = message.getMessageId(); String messageId = message.getMessageId();
if (messageId != null) { if (messageId != null) {
cv.put("message_id", messageId); 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; final long newId;
if (threadInfo.id != -1) { if (threadInfo.msgId != -1) {
// There already existed an empty message in the target folder. // There already existed an empty message in the target folder.
// Let's use it as placeholder. // Let's use it as placeholder.
newId = threadInfo.id; newId = threadInfo.msgId;
db.update("messages", cv, "id = ?", db.update("messages", cv, "id = ?",
new String[] { Long.toString(newId) }); new String[] { Long.toString(newId) });
@ -2143,27 +2227,14 @@ public class LocalStore extends Store implements Serializable {
} }
/* /*
* Replace all "thread links" to the original message with links to * Update old entry in 'threads' table to point to the newly
* the placeholder message. * created placeholder.
*/ */
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.clear();
cv.put("thread_root", newId); cv.put("message_id", newId);
db.update("messages", cv, "folder_id = ? AND thread_root = ?", db.update("threads", cv, "id = ?",
whereArgs); new String[] { Long.toString(lMessage.getThreadId()) });
cv.clear();
cv.put("thread_parent", newId);
db.update("messages", cv, "folder_id = ? AND thread_parent = ?",
whereArgs);
} }
} catch (MessagingException e) { } catch (MessagingException e) {
throw new WrappedException(e); throw new WrappedException(e);
@ -2285,21 +2356,23 @@ public class LocalStore extends Store implements Serializable {
} }
private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId) { private ThreadInfo getThreadInfo(SQLiteDatabase db, String messageId) {
Cursor cursor = db.query("messages", String sql = "SELECT t.id, t.message_id, t.root, t.parent " +
new String[] { "id", "thread_root", "thread_parent" }, "FROM threads t " +
"folder_id=? AND message_id=?", "JOIN messages m ON (t.message_id = m.id) " +
new String[] { Long.toString(mFolderId), messageId }, "WHERE m.folder_id = ? AND m.message_id = ?";
null, null, null); String[] selectionArgs = { Long.toString(mFolderId), messageId };
Cursor cursor = db.rawQuery(sql, selectionArgs);
if (cursor != null) { if (cursor != null) {
try { try {
if (cursor.getCount() == 1) { if (cursor.getCount() == 1) {
cursor.moveToFirst(); cursor.moveToFirst();
long id = cursor.getLong(0); long threadId = cursor.getLong(0);
long rootId = (cursor.isNull(1)) ? -1 : cursor.getLong(1); long msgId = cursor.getLong(1);
long parentId = (cursor.isNull(2)) ? -1 : cursor.getLong(2); long rootId = (cursor.isNull(2)) ? -1 : cursor.getLong(2);
long parentId = (cursor.isNull(3)) ? -1 : cursor.getLong(3);
return new ThreadInfo(id, messageId, rootId, parentId); return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
} }
} finally { } finally {
cursor.close(); cursor.close();
@ -2374,7 +2447,7 @@ public class LocalStore extends Store implements Serializable {
if (oldMessageId == -1) { if (oldMessageId == -1) {
// This is a new message. Do the message threading. // This is a new message. Do the message threading.
ThreadInfo threadInfo = doMessageThreading(db, message); ThreadInfo threadInfo = doMessageThreading(db, message);
oldMessageId = threadInfo.id; oldMessageId = threadInfo.msgId;
rootId = threadInfo.rootId; rootId = threadInfo.rootId;
parentId = threadInfo.parentId; parentId = threadInfo.parentId;
} }
@ -2437,25 +2510,32 @@ public class LocalStore extends Store implements Serializable {
cv.put("message_id", messageId); cv.put("message_id", messageId);
} }
if (rootId != -1) { long msgId;
cv.put("thread_root", rootId);
}
if (parentId != -1) {
cv.put("thread_parent", parentId);
}
long messageUid;
if (oldMessageId == -1) { if (oldMessageId == -1) {
messageUid = db.insert("messages", "uid", cv); msgId = db.insert("messages", "uid", cv);
// Create entry in 'threads' table
cv.clear();
cv.put("message_id", msgId);
if (rootId != -1) {
cv.put("root", rootId);
}
if (parentId != -1) {
cv.put("parent", parentId);
}
db.insert("threads", null, cv);
} else { } else {
db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) }); db.update("messages", cv, "id = ?", new String[] { Long.toString(oldMessageId) });
messageUid = oldMessageId; msgId = oldMessageId;
} }
for (Part attachment : attachments) { for (Part attachment : attachments) {
saveAttachment(messageUid, attachment, copy); saveAttachment(msgId, attachment, copy);
} }
saveHeaders(messageUid, (MimeMessage)message); saveHeaders(msgId, (MimeMessage)message);
} catch (Exception e) { } catch (Exception e) {
throw new MessagingException("Error appending message", e); throw new MessagingException("Error appending message", e);
} }
@ -2852,47 +2932,69 @@ public class LocalStore extends Store implements Serializable {
throw new MessagingException("Cannot call getUidFromMessageId on LocalFolder"); throw new MessagingException("Cannot call getUidFromMessageId on LocalFolder");
} }
private void clearMessagesWhere(final String whereClause, final String[] params) throws MessagingException { public void clearMessagesOlderThan(long cutoff) throws MessagingException {
open(OpenMode.READ_ONLY); open(OpenMode.READ_ONLY);
Message[] messages = LocalStore.this.getMessages( Message[] messages = LocalStore.this.getMessages(
null, null,
this, this,
"SELECT " + GET_MESSAGES_COLS + "FROM messages WHERE (empty IS NULL OR empty != 1) AND (" + whereClause + ")", "SELECT " + GET_MESSAGES_COLS +
params); "FROM messages " +
"JOIN threads ON (threads.message_id = messages.id) " +
"WHERE (empty IS NULL OR empty != 1) AND " +
"(folder_id = ? and date < ?)",
new String[] {
Long.toString(mFolderId), Long.toString(cutoff)
});
for (Message message : messages) { for (Message message : messages) {
deleteAttachments(message.getUid()); message.destroy();
} }
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException, UnavailableStorageException {
db.execSQL("DELETE FROM messages WHERE " + whereClause, params);
return null;
}
});
notifyChange(); notifyChange();
} }
public void clearMessagesOlderThan(long cutoff) throws MessagingException { public void clearAllMessages() throws MessagingException {
final String where = "folder_id = ? and date < ?"; final String[] folderIdArg = new String[] { Long.toString(mFolderId) };
final String[] params = new String[] {
Long.toString(mFolderId), Long.toString(cutoff)
};
clearMessagesWhere(where, params); open(OpenMode.READ_ONLY);
try {
database.execute(false, new DbCallback<Void>() {
@Override
public Void doDbWork(final SQLiteDatabase db) throws WrappedException {
try {
// Get UIDs for all messages to delete
Cursor cursor = db.query("messages", new String[] { "uid" },
"folder_id = ? AND (empty IS NULL OR empty != 1)",
folderIdArg, null, null, null);
try {
// Delete attachments of these messages
while (cursor.moveToNext()) {
deleteAttachments(cursor.getString(0));
}
} finally {
cursor.close();
} }
// Delete entries in 'threads' and 'messages'
db.execSQL("DELETE FROM threads WHERE message_id IN " +
"(SELECT id FROM messages WHERE folder_id = ?)", folderIdArg);
db.execSQL("DELETE FROM messages WHERE folder_id = ?", folderIdArg);
return null;
} catch (MessagingException e) {
throw new WrappedException(e);
}
}
});
} catch (WrappedException e) {
throw(MessagingException) e.getCause();
}
public void clearAllMessages() throws MessagingException { notifyChange();
final String where = "folder_id = ?";
final String[] params = new String[] {
Long.toString(mFolderId)
};
clearMessagesWhere(where, params);
setPushState(null); setPushState(null);
setLastPush(0); setLastPush(0);
setLastChecked(0); setLastChecked(0);
@ -3142,7 +3244,7 @@ public class LocalStore extends Store implements Serializable {
String messageId = message.getMessageId(); String messageId = message.getMessageId();
// If there's already an empty message in the database, update that // If there's already an empty message in the database, update that
long id = getDatabaseIdByMessageId(db, messageId, true); ThreadInfo msgThreadInfo = getThreadInfo(db, messageId);
// Get the message IDs from the "References" header line // Get the message IDs from the "References" header line
String[] referencesArray = message.getHeader("References"); String[] referencesArray = message.getHeader("References");
@ -3168,56 +3270,73 @@ public class LocalStore extends Store implements Serializable {
if (messageIds == null) { if (messageIds == null) {
// This is not a reply, nothing to do for us. // This is not a reply, nothing to do for us.
return new ThreadInfo(id, messageId, -1, -1); return (msgThreadInfo != null) ?
msgThreadInfo : new ThreadInfo(-1, -1, messageId, -1, -1);
} }
for (String reference : messageIds) { for (String reference : messageIds) {
ThreadInfo threadInfo = getThreadInfo(db, reference); ThreadInfo threadInfo = getThreadInfo(db, reference);
if (threadInfo == null) { if (threadInfo == null) {
// Create placeholder message // Create placeholder message in 'messages' table
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put("message_id", reference); cv.put("message_id", reference);
cv.put("folder_id", mFolderId); cv.put("folder_id", mFolderId);
cv.put("empty", 1); cv.put("empty", 1);
long newMsgId = db.insert("messages", null, cv);
// Create entry in 'threads' table
cv.clear();
cv.put("message_id", newMsgId);
if (rootId != -1) { if (rootId != -1) {
cv.put("thread_root", rootId); cv.put("root", rootId);
} }
if (parentId != -1) { if (parentId != -1) {
cv.put("thread_parent", parentId); cv.put("parent", parentId);
} }
parentId = db.insert("messages", null, cv); parentId = db.insert("threads", null, cv);
if (rootId == -1) { if (rootId == -1) {
rootId = parentId; rootId = parentId;
} }
} else { } else {
if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.id) { if (rootId != -1 && threadInfo.rootId == -1 && rootId != threadInfo.threadId) {
// We found an existing root container that is not // We found an existing root container that is not
// the root of our current path (References). // the root of our current path (References).
// Connect it to the current parent. // Connect it to the current parent.
// Let all children know who's the new root // Let all children know who's the new root
ContentValues cv = new ContentValues(); ContentValues cv = new ContentValues();
cv.put("thread_root", rootId); cv.put("root", rootId);
db.update("messages", cv, "thread_root=?", db.update("threads", cv, "root = ?",
new String[] { Long.toString(threadInfo.id) }); new String[] { Long.toString(threadInfo.threadId) });
// Connect the message to the current parent // Connect the message to the current parent
cv.put("thread_parent", parentId); cv.put("parent", parentId);
db.update("messages", cv, "id=?", db.update("threads", cv, "id = ?",
new String[] { Long.toString(threadInfo.id) }); new String[] { Long.toString(threadInfo.threadId) });
} else { } else {
rootId = (threadInfo.rootId == -1) ? threadInfo.id : threadInfo.rootId; rootId = (threadInfo.rootId == -1) ?
threadInfo.threadId : threadInfo.rootId;
} }
parentId = threadInfo.id; parentId = threadInfo.threadId;
} }
} }
//TODO: set in-reply-to "link" even if one already exists //TODO: set in-reply-to "link" even if one already exists
return new ThreadInfo(id, messageId, rootId, parentId); long threadId;
long msgId;
if (msgThreadInfo != null) {
threadId = msgThreadInfo.threadId;
msgId = msgThreadInfo.msgId;
} else {
threadId = -1;
msgId = -1;
}
return new ThreadInfo(threadId, msgId, messageId, rootId, parentId);
} }
public List<Message> extractNewMessages(final List<Message> messages) public List<Message> extractNewMessages(final List<Message> messages)
@ -3328,8 +3447,8 @@ public class LocalStore extends Store implements Serializable {
private boolean mHeadersLoaded = false; private boolean mHeadersLoaded = false;
private boolean mMessageDirty = false; private boolean mMessageDirty = false;
private long mThreadId;
private long mRootId; private long mRootId;
private long mParentId;
public LocalMessage() { public LocalMessage() {
} }
@ -3385,8 +3504,8 @@ public class LocalStore extends Store implements Serializable {
this.mFolder = f; this.mFolder = f;
} }
mRootId = (cursor.isNull(15)) ? -1 : cursor.getLong(15); mThreadId = (cursor.isNull(15)) ? -1 : cursor.getLong(15);
mParentId = (cursor.isNull(16)) ? -1 : cursor.getLong(16); mRootId = (cursor.isNull(16)) ? -1 : cursor.getLong(16);
boolean deleted = (cursor.getInt(17) == 1); boolean deleted = (cursor.getInt(17) == 1);
boolean read = (cursor.getInt(18) == 1); boolean read = (cursor.getInt(18) == 1);
@ -3656,17 +3775,9 @@ public class LocalStore extends Store implements Serializable {
localFolder.deleteAttachments(mId); localFolder.deleteAttachments(mId);
String id = Long.toString(mId); if (hasThreadChildren(db, mId)) {
// This message has children in the thread structure so we need to
// Check if this message has children in the thread hierarchy // make it an empty message.
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(); ContentValues cv = new ContentValues();
cv.put("id", mId); cv.put("id", mId);
cv.put("folder_id", localFolder.getId()); cv.put("folder_id", localFolder.getId());
@ -3674,79 +3785,37 @@ public class LocalStore extends Store implements Serializable {
cv.put("message_id", getMessageId()); cv.put("message_id", getMessageId());
cv.put("empty", 1); 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); db.replace("messages", null, cv);
// Nothing else to do // Nothing else to do
return null; return null;
} }
} finally {
cursor.close();
}
long parentId = getParentId(); // Get the message ID of the parent message if it's empty
long currentId = getEmptyThreadParent(db, mId);
// Check if 'parentId' is empty // Delete the placeholder message
cursor = db.query("messages", new String[] { "id" }, deleteMessageRow(db, mId);
"id = ? AND empty = 1",
new String[] { Long.toString(parentId) },
null, null, null);
try { /*
if (cursor.getCount() == 0) { * Walk the thread tree to delete all empty parents without children
// If the message isn't empty we skip the loop below */
parentId = -1;
}
} finally {
cursor.close();
}
while (parentId != -1) { while (currentId != -1) {
String parentIdString = Long.toString(parentId); if (hasThreadChildren(db, currentId)) {
// We made sure there are no empty leaf nodes and can stop now.
// 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; break;
} }
} finally {
cursor.close(); // Get ID of the (empty) parent for the next iteration
} long newId = getEmptyThreadParent(db, currentId);
// Delete the empty message
deleteMessageRow(db, currentId);
currentId = newId;
} }
// Remove the placeholder message
db.delete("messages", "id = ?", new String[] { id });
} catch (MessagingException e) { } catch (MessagingException e) {
throw new WrappedException(e); throw new WrappedException(e);
} }
@ -3760,6 +3829,77 @@ public class LocalStore extends Store implements Serializable {
notifyChange(); notifyChange();
} }
/**
* Get ID of the the given message's parent if the parent is an empty message.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to get the parent for.
*
* @return Message ID of the parent message if there exists a parent and it is empty.
* Otherwise {@code -1}.
*/
private long getEmptyThreadParent(SQLiteDatabase db, long messageId) {
Cursor cursor = db.rawQuery(
"SELECT m.id " +
"FROM threads t1 " +
"JOIN threads t2 ON (t1.parent = t2.id) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"WHERE t1.message_id = ? AND m.empty = 1",
new String[] { Long.toString(messageId) });
try {
return (cursor.moveToFirst() && !cursor.isNull(0)) ? cursor.getLong(0) : -1;
} finally {
cursor.close();
}
}
/**
* Check whether or not a message has child messages in the thread structure.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to get the children for.
*
* @return {@code true} if the message has children. {@code false} otherwise.
*/
private boolean hasThreadChildren(SQLiteDatabase db, long messageId) {
Cursor cursor = db.rawQuery(
"SELECT COUNT(t2.id) " +
"FROM threads t1 " +
"JOIN threads t2 ON (t2.parent = t1.id) " +
"WHERE t1.message_id = ?",
new String[] { Long.toString(messageId) });
try {
return (cursor.moveToFirst() && !cursor.isNull(0) && cursor.getLong(0) > 0L);
} finally {
cursor.close();
}
}
/**
* Delete a message from the 'messages' and 'threads' tables.
*
* @param db
* {@link SQLiteDatabase} instance to access the database.
* @param messageId
* The database ID of the message to delete.
*/
private void deleteMessageRow(SQLiteDatabase db, long messageId) {
String[] idArg = { Long.toString(messageId) };
// Delete the message
db.delete("messages", "id = ?", idArg);
// Delete row in 'threads' table
// TODO: create trigger for 'messages' table to get rid of the row in 'threads' table
db.delete("threads", "message_id = ?", idArg);
}
private void loadHeaders() throws UnavailableStorageException { private void loadHeaders() throws UnavailableStorageException {
ArrayList<LocalMessage> messages = new ArrayList<LocalMessage>(); ArrayList<LocalMessage> messages = new ArrayList<LocalMessage>();
messages.add(this); messages.add(this);
@ -3818,12 +3958,12 @@ public class LocalStore extends Store implements Serializable {
return message; return message;
} }
public long getRootId() { public long getThreadId() {
return mRootId; return mThreadId;
} }
public long getParentId() { public long getRootId() {
return mParentId; return mRootId;
} }
} }
@ -3893,13 +4033,15 @@ public class LocalStore extends Store implements Serializable {
} }
static class ThreadInfo { static class ThreadInfo {
public final long id; public final long threadId;
public final long msgId;
public final String messageId; public final String messageId;
public final long rootId; public final long rootId;
public final long parentId; public final long parentId;
public ThreadInfo(long id, String messageId, long rootId, long parentId) { public ThreadInfo(long threadId, long msgId, String messageId, long rootId, long parentId) {
this.id = id; this.threadId = threadId;
this.msgId = msgId;
this.messageId = messageId; this.messageId = messageId;
this.rootId = rootId; this.rootId = rootId;
this.parentId = parentId; this.parentId = parentId;
@ -4045,8 +4187,18 @@ public class LocalStore extends Store implements Serializable {
* *
* @throws MessagingException * @throws MessagingException
*/ */
public void setFlag(final List<Long> messageIds, final Flag flag, public void setFlag(List<Long> messageIds, Flag flag, boolean newState, boolean threadRootIds)
final boolean newState, final boolean threadRootIds) throws MessagingException { throws MessagingException {
if (threadRootIds) {
setFlagForThreads(messageIds, flag, newState);
} else {
setFlag(messageIds, flag, newState);
}
}
public void setFlag(final List<Long> messageIds, final Flag flag, final boolean newState)
throws MessagingException {
final ContentValues cv = new ContentValues(); final ContentValues cv = new ContentValues();
@ -4090,11 +4242,67 @@ public class LocalStore extends Store implements Serializable {
db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet, db.update("messages", cv, "(empty IS NULL OR empty != 1) AND id" + selectionSet,
selectionArgs); selectionArgs);
if (threadRootIds) {
db.update("messages", cv, "(empty IS NULL OR empty != 1) AND thread_root" +
selectionSet, selectionArgs);
} }
@Override
public void postDbWork() {
notifyChange();
}
}, FLAG_UPDATE_BATCH_SIZE);
}
public void setFlagForThreads(final List<Long> messageIds, Flag flag, final boolean newState)
throws MessagingException {
final String flagColumn;
switch (flag) {
case SEEN: {
flagColumn = "read";
break;
}
case FLAGGED: {
flagColumn = "flagged";
break;
}
case ANSWERED: {
flagColumn = "answered";
break;
}
case FORWARDED: {
flagColumn = "forwarded";
break;
}
default: {
throw new IllegalArgumentException("Flag must be a special column flag");
}
}
doBatchSetSelection(new BatchSetSelection() {
@Override
public int getListSize() {
return messageIds.size();
}
@Override
public String getListItem(int index) {
return Long.toString(messageIds.get(index));
}
@Override
public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
throws UnavailableStorageException {
db.execSQL("UPDATE messages SET " + flagColumn + " = " + ((newState) ? "1" : "0") +
" WHERE id IN (" +
"SELECT m.id FROM messages h " +
"JOIN threads t1 ON (t1.message_id = h.id) " +
"JOIN threads t2 ON " +
"(t1.root IN (t2.id, t2.root) OR t1.id IN (t2.id, t2.root)) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
"AND h.id" + selectionSet + ")",
selectionArgs);
} }
@Override @Override
@ -4140,6 +4348,20 @@ public class LocalStore extends Store implements Serializable {
public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs) public void doDbWork(SQLiteDatabase db, String selectionSet, String[] selectionArgs)
throws UnavailableStorageException { throws UnavailableStorageException {
if (threadedList) {
String sql = "SELECT m.uid, f.name " +
"FROM messages h " +
"JOIN threads t1 ON (t1.message_id = h.id) " +
"JOIN threads t2 ON " +
"(t1.root IN (t2.id, t2.root) OR t1.id IN (t2.id, t2.root)) " +
"JOIN messages m ON (t2.message_id = m.id) " +
"JOIN folders f ON (m.folder_id = f.id) " +
"WHERE (m.empty IS NULL OR m.empty != 1) AND m.deleted = 0 " +
"AND h.id" + selectionSet;
getDataFromCursor(db.rawQuery(sql, selectionArgs));
} else {
String sqlPrefix = String sqlPrefix =
"SELECT m.uid, f.name " + "SELECT m.uid, f.name " +
"FROM messages m " + "FROM messages m " +
@ -4148,10 +4370,6 @@ public class LocalStore extends Store implements Serializable {
String sql = sqlPrefix + "m.id" + selectionSet; String sql = sqlPrefix + "m.id" + selectionSet;
getDataFromCursor(db.rawQuery(sql, selectionArgs)); getDataFromCursor(db.rawQuery(sql, selectionArgs));
if (threadedList) {
String threadSql = sqlPrefix + "m.thread_root" + selectionSet;
getDataFromCursor(db.rawQuery(threadSql, selectionArgs));
} }
} }

View File

@ -54,7 +54,7 @@ public class EmailProvider extends ContentProvider {
private static final int MESSAGE_BASE = 0; private static final int MESSAGE_BASE = 0;
private static final int MESSAGES = MESSAGE_BASE; 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 int MESSAGES_THREAD = MESSAGE_BASE + 2;
private static final int STATS_BASE = 100; private static final int STATS_BASE = 100;
private static final int STATS = STATS_BASE; private static final int STATS = STATS_BASE;
@ -78,8 +78,6 @@ public class EmailProvider extends ContentProvider {
MessageColumns.ATTACHMENT_COUNT, MessageColumns.ATTACHMENT_COUNT,
MessageColumns.FOLDER_ID, MessageColumns.FOLDER_ID,
MessageColumns.PREVIEW, MessageColumns.PREVIEW,
MessageColumns.THREAD_ROOT,
MessageColumns.THREAD_PARENT,
MessageColumns.READ, MessageColumns.READ,
MessageColumns.FLAGGED, MessageColumns.FLAGGED,
MessageColumns.ANSWERED, MessageColumns.ANSWERED,
@ -95,6 +93,8 @@ public class EmailProvider extends ContentProvider {
MessageColumns.ID MessageColumns.ID
}; };
private static final String FOLDERS_TABLE = "folders";
private static final String[] FOLDERS_COLUMNS = { private static final String[] FOLDERS_COLUMNS = {
FolderColumns.ID, FolderColumns.ID,
FolderColumns.NAME, FolderColumns.NAME,
@ -112,12 +112,21 @@ public class EmailProvider extends ContentProvider {
FolderColumns.DISPLAY_CLASS FolderColumns.DISPLAY_CLASS
}; };
private static final String THREADS_TABLE = "threads";
private static final String[] THREADS_COLUMNS = {
ThreadColumns.ID,
ThreadColumns.MESSAGE_ID,
ThreadColumns.ROOT,
ThreadColumns.PARENT
};
static { static {
UriMatcher matcher = sUriMatcher; UriMatcher matcher = sUriMatcher;
matcher.addURI(AUTHORITY, "account/*/messages", MESSAGES); 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); matcher.addURI(AUTHORITY, "account/*/thread/#", MESSAGES_THREAD);
matcher.addURI(AUTHORITY, "account/*/stats", STATS); matcher.addURI(AUTHORITY, "account/*/stats", STATS);
} }
@ -125,6 +134,8 @@ public class EmailProvider extends ContentProvider {
public interface SpecialColumns { public interface SpecialColumns {
public static final String ACCOUNT_UUID = "account_uuid"; public static final String ACCOUNT_UUID = "account_uuid";
public static final String THREAD_COUNT = "thread_count";
public static final String FOLDER_NAME = "name"; public static final String FOLDER_NAME = "name";
public static final String INTEGRATE = "integrate"; public static final String INTEGRATE = "integrate";
} }
@ -145,9 +156,6 @@ public class EmailProvider extends ContentProvider {
public static final String ATTACHMENT_COUNT = "attachment_count"; public static final String ATTACHMENT_COUNT = "attachment_count";
public static final String FOLDER_ID = "folder_id"; public static final String FOLDER_ID = "folder_id";
public static final String PREVIEW = "preview"; 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";
public static final String READ = "read"; public static final String READ = "read";
public static final String FLAGGED = "flagged"; public static final String FLAGGED = "flagged";
public static final String ANSWERED = "answered"; public static final String ANSWERED = "answered";
@ -179,6 +187,13 @@ public class EmailProvider extends ContentProvider {
public static final String DISPLAY_CLASS = "display_class"; public static final String DISPLAY_CLASS = "display_class";
} }
public interface ThreadColumns {
public static final String ID = "id";
public static final String MESSAGE_ID = "message_id";
public static final String ROOT = "root";
public static final String PARENT = "parent";
}
public interface StatsColumns { public interface StatsColumns {
public static final String UNREAD_COUNT = "unread_count"; public static final String UNREAD_COUNT = "unread_count";
public static final String FLAGGED_COUNT = "flagged_count"; public static final String FLAGGED_COUNT = "flagged_count";
@ -216,7 +231,8 @@ public class EmailProvider extends ContentProvider {
Cursor cursor = null; Cursor cursor = null;
switch (match) { switch (match) {
case MESSAGES: case MESSAGES:
case MESSAGES_THREADED: { case MESSAGES_THREADED:
case MESSAGES_THREAD: {
List<String> segments = uri.getPathSegments(); List<String> segments = uri.getPathSegments();
String accountUuid = segments.get(1); String accountUuid = segments.get(1);
@ -238,11 +254,16 @@ public class EmailProvider extends ContentProvider {
} else if (match == MESSAGES_THREADED) { } else if (match == MESSAGES_THREADED) {
cursor = getThreadedMessages(accountUuid, dbProjection, selection, cursor = getThreadedMessages(accountUuid, dbProjection, selection,
selectionArgs, sortOrder); selectionArgs, sortOrder);
} else if (match == MESSAGES_THREAD) {
String threadId = segments.get(3);
cursor = getThread(accountUuid, dbProjection, threadId, sortOrder);
} else { } else {
throw new RuntimeException("Not implemented"); throw new RuntimeException("Not implemented");
} }
cursor.setNotificationUri(contentResolver, uri); Uri notificationUri = Uri.withAppendedPath(CONTENT_URI, "account/" + accountUuid +
"/messages");
cursor.setNotificationUri(contentResolver, notificationUri);
cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection, cursor = new SpecialColumnsCursor(new IdTrickeryCursor(cursor), projection,
specialColumns); specialColumns);
@ -328,6 +349,7 @@ public class EmailProvider extends ContentProvider {
} }
query.append(" FROM messages m " + query.append(" FROM messages m " +
"JOIN threads t ON (t.message_id = m.id) " +
"LEFT JOIN folders f ON (m.folder_id = f.id) " + "LEFT JOIN folders f ON (m.folder_id = f.id) " +
"WHERE "); "WHERE ");
query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS, query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS,
@ -374,14 +396,20 @@ public class EmailProvider extends ContentProvider {
if (MessageColumns.DATE.equals(columnName)) { if (MessageColumns.DATE.equals(columnName)) {
query.append("MAX(m.date) AS " + MessageColumns.DATE); query.append("MAX(m.date) AS " + MessageColumns.DATE);
} else if (MessageColumns.THREAD_COUNT.equals(columnName)) { } else if (SpecialColumns.THREAD_COUNT.equals(columnName)) {
query.append("COUNT(h.id) AS " + MessageColumns.THREAD_COUNT); query.append("COUNT(h.id) AS " + SpecialColumns.THREAD_COUNT);
} else if (SpecialColumns.FOLDER_NAME.equals(columnName)) { } else if (SpecialColumns.FOLDER_NAME.equals(columnName)) {
query.append("f." + SpecialColumns.FOLDER_NAME + " AS " + query.append("f." + SpecialColumns.FOLDER_NAME + " AS " +
SpecialColumns.FOLDER_NAME); SpecialColumns.FOLDER_NAME);
} else if (SpecialColumns.INTEGRATE.equals(columnName)) { } else if (SpecialColumns.INTEGRATE.equals(columnName)) {
query.append("f." + SpecialColumns.INTEGRATE + " AS " + query.append("f." + SpecialColumns.INTEGRATE + " AS " +
SpecialColumns.INTEGRATE); SpecialColumns.INTEGRATE);
} else if (ThreadColumns.ROOT.equals(columnName)) {
// Always return the thread ID of the root message (even for the root
// message itself)
query.append("CASE WHEN t2." + ThreadColumns.ROOT + " IS NULL THEN " +
"t2." + ThreadColumns.ID + " ELSE t2." + ThreadColumns.ROOT +
" END AS " + ThreadColumns.ROOT);
} else { } else {
query.append("m."); query.append("m.");
query.append(columnName); query.append(columnName);
@ -391,8 +419,10 @@ public class EmailProvider extends ContentProvider {
} }
query.append( query.append(
" FROM messages h JOIN messages m " + " FROM messages h " +
"ON (h.id = m.thread_root OR h.id = m.id) "); "LEFT JOIN threads t1 ON (t1.message_id = h.id) " +
"LEFT JOIN threads t2 ON (t1.id = t2.id OR t1.id = t2.root) " +
"LEFT JOIN messages m ON (m.id = t2.message_id) ");
if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) { if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) {
query.append("LEFT JOIN folders f ON (m.folder_id = f.id) "); query.append("LEFT JOIN folders f ON (m.folder_id = f.id) ");
@ -400,9 +430,9 @@ public class EmailProvider extends ContentProvider {
query.append( query.append(
"WHERE " + "WHERE " +
"(m.deleted = 0 AND " + "(t1.root IS NULL AND " +
"(m.empty IS NULL OR m.empty != 1) AND " + "m.deleted = 0 AND " +
"h.thread_root IS NULL) "); "(m.empty IS NULL OR m.empty != 1)) ");
if (!StringUtils.isNullOrEmpty(selection)) { if (!StringUtils.isNullOrEmpty(selection)) {
query.append("AND ("); query.append("AND (");
@ -427,6 +457,63 @@ public class EmailProvider extends ContentProvider {
} }
} }
protected Cursor getThread(String accountUuid, final String[] projection, final String threadId,
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.ID.equals(columnName)) {
query.append("m." + MessageColumns.ID + " AS " + MessageColumns.ID);
} else {
query.append(columnName);
}
}
query.append(" FROM " + THREADS_TABLE + " t JOIN " + MESSAGES_TABLE + " m " +
"ON (m." + MessageColumns.ID + " = t." + ThreadColumns.MESSAGE_ID +
") ");
if (Utility.arrayContainsAny(projection, (Object[]) FOLDERS_COLUMNS)) {
query.append("LEFT JOIN " + FOLDERS_TABLE + " f " +
"ON (m." + MessageColumns.FOLDER_ID + " = f." + FolderColumns.ID +
") ");
}
query.append("WHERE (t." + ThreadColumns.ID + " = ? OR " +
ThreadColumns.ROOT + " = ?) AND " +
InternalMessageColumns.DELETED + " = 0 AND (" +
InternalMessageColumns.EMPTY + " IS NULL OR " +
InternalMessageColumns.EMPTY + " != 1)");
query.append(" ORDER BY ");
query.append(SqlQueryBuilder.addPrefixToSelection(FIXUP_MESSAGES_COLUMNS,
"m.", sortOrder));
return db.rawQuery(query.toString(), new String[] { threadId, threadId });
}
});
} catch (UnavailableStorageException e) {
throw new RuntimeException("Storage not available", e);
}
}
private Cursor getAccountStats(String accountUuid, String[] columns, private Cursor getAccountStats(String accountUuid, String[] columns,
final String selection, final String[] selectionArgs) { final String selection, final String[] selectionArgs) {

View File

@ -76,7 +76,7 @@ public interface SearchSpecification extends Parcelable {
MESSAGE_CONTENTS, MESSAGE_CONTENTS,
ATTACHMENT_COUNT, ATTACHMENT_COUNT,
DELETED, DELETED,
THREAD_ROOT, THREAD_ID,
ID, ID,
INTEGRATE, INTEGRATE,
READ, READ,

View File

@ -66,6 +66,12 @@ public class SqlQueryBuilder {
} }
break; break;
} }
case THREAD_ID: {
query.append("threads.id = ? OR threads.root = ?");
selectionArgs.add(condition.value);
selectionArgs.add(condition.value);
break;
}
default: { default: {
appendCondition(condition, query, selectionArgs); appendCondition(condition, query, selectionArgs);
} }
@ -149,10 +155,6 @@ public class SqlQueryBuilder {
columnName = "subject"; columnName = "subject";
break; break;
} }
case THREAD_ROOT: {
columnName = "thread_root";
break;
}
case TO: { case TO: {
columnName = "to_list"; columnName = "to_list";
break; break;
@ -177,6 +179,7 @@ public class SqlQueryBuilder {
columnName = "display_class"; columnName = "display_class";
break; break;
} }
case THREAD_ID:
case FOLDER: case FOLDER:
case SEARCHABLE: { case SEARCHABLE: {
// Special cases handled in buildWhereClauseInternal() // Special cases handled in buildWhereClauseInternal()
@ -258,7 +261,7 @@ public class SqlQueryBuilder {
case FOLDER: case FOLDER:
case ID: case ID:
case INTEGRATE: case INTEGRATE:
case THREAD_ROOT: case THREAD_ID:
case READ: case READ:
case FLAGGED: { case FLAGGED: {
return true; return true;
@ -272,7 +275,7 @@ public class SqlQueryBuilder {
public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) { public static String addPrefixToSelection(String[] columnNames, String prefix, String selection) {
String result = selection; String result = selection;
for (String columnName : columnNames) { for (String columnName : columnNames) {
result = result.replaceAll("\\b" + columnName + "\\b", prefix + columnName); result = result.replaceAll("(?<=^|[^\\.])\\b" + columnName + "\\b", prefix + columnName);
} }
return result; return result;