1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-12-24 08:38:51 -05:00

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
This commit is contained in:
cketti 2012-10-08 22:51:29 +02:00
parent e66dd3d521
commit 63b6b497a0
7 changed files with 684 additions and 57 deletions

View File

@ -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" />
<TextView
android:id="@+id/thread_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="6dip"
android:layout_marginRight="12dip"
android:layout_alignParentRight="true"
android:layout_below="@+id/subject"
android:gravity="center_vertical|center_horizontal"
android:paddingLeft="4dip"
android:paddingRight="4dip"
android:paddingTop="2dip"
android:paddingBottom="2dip"
android:background="#50000000"
android:focusable="false" />
<TextView
android:id="@+id/date"
android:layout_width="wrap_content"
@ -65,7 +83,4 @@
android:textAppearance="?android:attr/textAppearanceSmall"
android:textColor="?android:attr/textColorSecondary" />
</RelativeLayout>

View File

@ -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);
}
}

View File

@ -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");

View File

@ -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);

View File

@ -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<String> extractMessageIds(final String text) {
List<String> messageIds = new ArrayList<String>();
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;
}
}

View File

@ -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");

View File

@ -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<Message[]>() {
@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<Message[]>() {
@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<Part> 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<String> 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<String>(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<Void>() {
@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;
}
}
}