New search implementation for IMAP in MessagingController

* Add protocolSearchInfo column to Message table; this can be used
  to store information related to search results.  For IMAP, we
  store the serverId of the mailbox that the message lives in on
  the server
* Add upgrade code for this column
* Change MessagingController to use the proper serverId for remote
  operations, depending on whether the Message is a search result
  or not
* Fix some smaller issues with earlier code

Change-Id: I0c7f1d89a4659b95701d02646c0e8426680e2f6a
This commit is contained in:
Marc Blank 2011-06-23 10:52:21 -07:00
parent 53300963ff
commit 9a01353f14
6 changed files with 117 additions and 70 deletions

View File

@ -479,6 +479,11 @@ public abstract class EmailContent {
public static final String MEETING_INFO = "meetingInfo";
// A text "snippet" derived from the body of the message
public static final String SNIPPET = "snippet";
// A column that can be used by sync adapters to store search-related information about
// a retrieved message (the messageKey for search results will be a TYPE_SEARCH mailbox
// and the sync adapter might, for example, need more information about the original source
// of the message)
public static final String PROTOCOL_SEARCH_INFO = "protocolSearchInfo";
}
public static final class Message extends EmailContent implements SyncColumns, MessageColumns {
@ -523,6 +528,7 @@ public abstract class EmailContent {
public static final int CONTENT_SERVER_TIMESTAMP_COLUMN = 19;
public static final int CONTENT_MEETING_INFO_COLUMN = 20;
public static final int CONTENT_SNIPPET_COLUMN = 21;
public static final int CONTENT_PROTOCOL_SEARCH_INFO_COLUMN = 22;
public static final String[] CONTENT_PROJECTION = new String[] {
RECORD_ID,
@ -536,7 +542,7 @@ public abstract class EmailContent {
MessageColumns.TO_LIST, MessageColumns.CC_LIST,
MessageColumns.BCC_LIST, MessageColumns.REPLY_TO_LIST,
SyncColumns.SERVER_TIMESTAMP, MessageColumns.MEETING_INFO,
MessageColumns.SNIPPET
MessageColumns.SNIPPET, MessageColumns.PROTOCOL_SEARCH_INFO
};
public static final int LIST_ID_COLUMN = 0;
@ -669,6 +675,8 @@ public abstract class EmailContent {
public String mSnippet;
public String mProtocolSearchInfo;
// The following transient members may be used while building and manipulating messages,
// but they are NOT persisted directly by EmailProvider
transient public String mText;
@ -679,6 +687,7 @@ public abstract class EmailContent {
transient public ArrayList<Attachment> mAttachments = null;
transient public String mIntroText;
// Values used in mFlagRead
public static final int UNREAD = 0;
public static final int READ = 1;
@ -762,6 +771,7 @@ public abstract class EmailContent {
values.put(MessageColumns.SNIPPET, mSnippet);
values.put(MessageColumns.PROTOCOL_SEARCH_INFO, mProtocolSearchInfo);
return values;
}
@ -795,6 +805,7 @@ public abstract class EmailContent {
mReplyTo = cursor.getString(CONTENT_REPLY_TO_COLUMN);
mMeetingInfo = cursor.getString(CONTENT_MEETING_INFO_COLUMN);
mSnippet = cursor.getString(CONTENT_SNIPPET_COLUMN);
mProtocolSearchInfo = cursor.getString(CONTENT_PROTOCOL_SEARCH_INFO_COLUMN);
}
public boolean update() {

View File

@ -97,7 +97,7 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns
private static final String MAILBOX_TYPE_SELECTION =
MailboxColumns.TYPE + " =?";
/** Selection by server pathname for a given account */
private static final String PATH_AND_ACCOUNT_SELECTION =
public static final String PATH_AND_ACCOUNT_SELECTION =
MailboxColumns.SERVER_ID + "=? and " + MailboxColumns.ACCOUNT_KEY + "=?";
private static final String[] MAILBOX_SUM_OF_UNREAD_COUNT_PROJECTION = new String [] {

View File

@ -946,9 +946,6 @@ public class Controller {
ContentValues cv = new ContentValues();
// For now, use the actual query as the name of the mailbox
cv.put(Mailbox.DISPLAY_NAME, searchParams.mFilter);
// But use the server id of the actual mailbox we're searching; this allows full
// message loading to work normally (clever, huh?)
cv.put(MailboxColumns.SERVER_ID, actualMailbox.mServerId);
resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailboxId),
cv, null, null);
}

View File

@ -23,6 +23,7 @@ import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Process;
import android.text.TextUtils;
import android.util.Log;
import com.android.email.mail.Sender;
@ -133,6 +134,14 @@ public class MessagingController implements Runnable {
private final Context mContext;
private final Controller mController;
/**
* Simple cache for last search result mailbox by account and serverId, since the most common
* case will be repeated use of the same mailbox
*/
private long mLastSearchAccountKey = Account.NO_ACCOUNT;
private String mLastSearchServerId = null;
private Mailbox mLastSearchRemoteMailbox = null;
protected MessagingController(Context _context, Controller _controller) {
mContext = _context.getApplicationContext();
mController = _controller;
@ -280,6 +289,7 @@ public class MessagingController implements Runnable {
case Mailbox.TYPE_OUTBOX:
case Mailbox.TYPE_SENT:
case Mailbox.TYPE_TRASH:
case Mailbox.TYPE_SEARCH:
// Never, ever delete special mailboxes
break;
default:
@ -668,6 +678,10 @@ public class MessagingController implements Runnable {
localMessage.mMailboxKey = destMailboxId;
// We load 50k or so; maybe it's complete, maybe not...
int flag = EmailContent.Message.FLAG_LOADED_COMPLETE;
// We store the serverId of the source mailbox into protocolSearchInfo
// This will be used by loadMessageForView, etc. to use the proper remote
// folder
localMessage.mProtocolSearchInfo = mailbox.mServerId;
if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) {
flag = EmailContent.Message.FLAG_LOADED_PARTIAL;
}
@ -1057,6 +1071,45 @@ public class MessagingController implements Runnable {
processPendingUpdatesSynchronous(account, resolver, accountIdArgs);
}
/**
* Get the mailbox corresponding to the remote location of a message; this will normally be
* the mailbox whose _id is mailboxKey, except for search results, where we must look it up
* by serverId
* @param message the message in question
* @return the mailbox in which the message resides on the server
*/
private Mailbox getRemoteMailboxForMessage(EmailContent.Message message) {
// If this is a search result, use the protocolSearchInfo field to get the server info
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
long accountKey = message.mAccountKey;
String protocolSearchInfo = message.mProtocolSearchInfo;
if (accountKey == mLastSearchAccountKey &&
protocolSearchInfo.equals(mLastSearchServerId)) {
return mLastSearchRemoteMailbox;
}
Cursor c = mContext.getContentResolver().query(Mailbox.CONTENT_URI,
Mailbox.CONTENT_PROJECTION, Mailbox.PATH_AND_ACCOUNT_SELECTION,
new String[] {protocolSearchInfo, Long.toString(accountKey)},
null);
try {
if (c.moveToNext()) {
Mailbox mailbox = new Mailbox();
mailbox.restore(c);
mLastSearchAccountKey = accountKey;
mLastSearchServerId = protocolSearchInfo;
mLastSearchRemoteMailbox = mailbox;
return mailbox;
} else {
return null;
}
} finally {
c.close();
}
} else {
return Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey);
}
}
/**
* Scan for messages that are in the Message_Deletes table, look for differences that
* we can deal with, and do the work.
@ -1075,8 +1128,6 @@ public class MessagingController implements Runnable {
try {
// Defer setting up the store until we know we need to access it
Store remoteStore = null;
// Demand load mailbox (note order-by to reduce thrashing here)
Mailbox mailbox = null;
// loop through messages marked as deleted
while (deletes.moveToNext()) {
boolean deleteFromTrash = false;
@ -1086,24 +1137,23 @@ public class MessagingController implements Runnable {
if (oldMessage != null) {
lastMessageId = oldMessage.mId;
if (mailbox == null || mailbox.mId != oldMessage.mMailboxKey) {
mailbox = Mailbox.restoreMailboxWithId(mContext, oldMessage.mMailboxKey);
if (mailbox == null) {
continue; // Mailbox removed. Move to the next message.
}
Mailbox mailbox = getRemoteMailboxForMessage(oldMessage);
if (mailbox == null) {
continue; // Mailbox removed. Move to the next message.
}
deleteFromTrash = mailbox.mType == Mailbox.TYPE_TRASH;
}
// Load the remote store if it will be needed
if (remoteStore == null && deleteFromTrash) {
remoteStore = Store.getInstance(account, mContext, null);
}
// Load the remote store if it will be needed
if (remoteStore == null && deleteFromTrash) {
remoteStore = Store.getInstance(account, mContext, null);
}
// Dispatch here for specific change types
if (deleteFromTrash) {
// Move message to trash
processPendingDeleteFromTrash(remoteStore, account, mailbox, oldMessage);
// Dispatch here for specific change types
if (deleteFromTrash) {
// Move message to trash
processPendingDeleteFromTrash(remoteStore, account, mailbox, oldMessage);
}
}
// Finally, delete the update
@ -1111,7 +1161,6 @@ public class MessagingController implements Runnable {
oldMessage.mId);
resolver.delete(uri, null, null);
}
} catch (MessagingException me) {
// Presumably an error here is an account connection failure, so there is
// no point in continuing through the rest of the pending updates.
@ -1266,11 +1315,9 @@ public class MessagingController implements Runnable {
EmailContent.Message newMessage =
EmailContent.Message.restoreMessageWithId(mContext, oldMessage.mId);
if (newMessage != null) {
if (mailbox == null || mailbox.mId != newMessage.mMailboxKey) {
mailbox = Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey);
if (mailbox == null) {
continue; // Mailbox removed. Move to the next message.
}
mailbox = Mailbox.restoreMailboxWithId(mContext, newMessage.mMailboxKey);
if (mailbox == null) {
continue; // Mailbox removed. Move to the next message.
}
if (oldMessage.mMailboxKey != newMessage.mMailboxKey) {
if (mailbox.mType == Mailbox.TYPE_TRASH) {
@ -1382,7 +1429,11 @@ public class MessagingController implements Runnable {
private void processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead,
boolean changeFlagged, boolean changeMailbox, EmailContent.Message oldMessage,
final EmailContent.Message newMessage) throws MessagingException {
Mailbox newMailbox = null;
// New mailbox is the mailbox this message WILL be in (same as the one it WAS in if it isn't
// being moved
Mailbox newMailbox = mailbox;
// Mailbox is the original remote mailbox (the one we're acting on)
mailbox = getRemoteMailboxForMessage(oldMessage);
// 0. No remote update if the message is local-only
if (newMessage.mServerId == null || newMessage.mServerId.equals("")
@ -1390,18 +1441,6 @@ public class MessagingController implements Runnable {
return;
}
// 0.5 If the mailbox has changed, use the original mailbox for operations
// After any flag changes (which we execute in the original mailbox), we then
// copy the message to the new mailbox
if (changeMailbox) {
newMailbox = mailbox;
mailbox = Mailbox.restoreMailboxWithId(mContext, oldMessage.mMailboxKey);
}
if (mailbox == null) {
return;
}
// 1. No remote update for DRAFTS or OUTBOX
if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) {
return;
@ -1486,7 +1525,7 @@ public class MessagingController implements Runnable {
// 1. Escape early if we can't find the local mailbox
// TODO smaller projection here
Mailbox oldMailbox = Mailbox.restoreMailboxWithId(mContext, oldMessage.mMailboxKey);
Mailbox oldMailbox = getRemoteMailboxForMessage(oldMessage);
if (oldMailbox == null) {
// can't find old mailbox, it may have been deleted. just return.
return;
@ -1784,7 +1823,6 @@ public class MessagingController implements Runnable {
}
// 2. Open the remote folder.
// TODO all of these could be narrower projections
// TODO combine with common code in loadAttachment
Account account =
Account.restoreAccountWithId(mContext, message.mAccountKey);
@ -1797,40 +1835,26 @@ public class MessagingController implements Runnable {
Store remoteStore =
Store.getInstance(account, mContext, null);
Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId);
String remoteServerId = mailbox.mServerId;
// If this is a search result, use the protocolSearchInfo field to get the
// correct remote location
if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) {
remoteServerId = message.mProtocolSearchInfo;
}
Folder remoteFolder = remoteStore.getFolder(remoteServerId);
remoteFolder.open(OpenMode.READ_WRITE, null);
// 3. Not supported, because IMAP & POP don't use it: structure prefetch
// if (remoteStore.requireStructurePrefetch()) {
// // For remote stores that require it, prefetch the message structure.
// FetchProfile fp = new FetchProfile();
// fp.add(FetchProfile.Item.STRUCTURE);
// localFolder.fetch(new Message[] { message }, fp, null);
//
// ArrayList<Part> viewables = new ArrayList<Part>();
// ArrayList<Part> attachments = new ArrayList<Part>();
// MimeUtility.collectParts(message, viewables, attachments);
// fp.clear();
// for (Part part : viewables) {
// fp.add(part);
// }
//
// remoteFolder.fetch(new Message[] { message }, fp, null);
//
// // Store the updated message locally
// localFolder.updateMessage((LocalMessage)message);
// 4. Set up to download the entire message
Message remoteMessage = remoteFolder.getMessage(message.mServerId);
// 3. Set up to download the entire message
Message remoteMessage = remoteFolder.getMessage(remoteServerId);
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.BODY);
remoteFolder.fetch(new Message[] { remoteMessage }, fp, null);
// 5. Write to provider
// 4. Write to provider
copyOneMessageToProvider(remoteMessage, account, mailbox,
EmailContent.Message.FLAG_LOADED_COMPLETE);
// 6. Notify UI
// 5. Notify UI
mListeners.loadMessageForViewFinished(messageId);
} catch (MessagingException me) {

View File

@ -601,7 +601,6 @@ class ImapFolder extends Folder {
// TODO Should we accept "RFC822" as well??
ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true);
String bodyText = body.getString();
Log.v(Logging.LOG_TAG, bodyText);
InputStream bodyStream = body.getAsStream();
message.parse(bodyStream);
}

View File

@ -157,8 +157,9 @@ public class EmailProvider extends ContentProvider {
// Version 24: Add column to hostauth table for client cert alias
// Version 25: Added QuickResponse table
// Version 26: Update IMAP accounts to add FLAG_SUPPORTS_SEARCH flag
// Version 27: Add protocolSearchInfo to Message table
public static final int DATABASE_VERSION = 26;
public static final int DATABASE_VERSION = 27;
// Any changes to the database format *must* include update-in-place code.
// Original version: 2
@ -463,7 +464,8 @@ public class EmailProvider extends ContentProvider {
+ MessageColumns.BCC_LIST + " text, "
+ MessageColumns.REPLY_TO_LIST + " text, "
+ MessageColumns.MEETING_INFO + " text, "
+ MessageColumns.SNIPPET + " text"
+ MessageColumns.SNIPPET + " text, "
+ MessageColumns.PROTOCOL_SEARCH_INFO + " text"
+ ");";
// This String and the following String MUST have the same columns, except for the type
@ -1261,6 +1263,20 @@ public class EmailProvider extends ContentProvider {
upgradeFromVersion25ToVersion26(db);
oldVersion = 26;
}
if (oldVersion == 26) {
try {
db.execSQL("alter table " + Message.TABLE_NAME
+ " add column " + Message.PROTOCOL_SEARCH_INFO + " text;");
db.execSQL("alter table " + Message.DELETED_TABLE_NAME
+ " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";");
db.execSQL("alter table " + Message.UPDATED_TABLE_NAME
+ " add column " + Message.PROTOCOL_SEARCH_INFO +" text" + ";");
} catch (SQLException e) {
// Shouldn't be needed unless we're debugging and interrupt the process
Log.w(TAG, "Exception upgrading EmailProvider.db from 26 to 27 " + e);
}
oldVersion = 27;
}
}
@Override