diff --git a/emailcommon/src/com/android/emailcommon/mail/Folder.java b/emailcommon/src/com/android/emailcommon/mail/Folder.java index 0ccad5881..9bdbdd44a 100644 --- a/emailcommon/src/com/android/emailcommon/mail/Folder.java +++ b/emailcommon/src/com/android/emailcommon/mail/Folder.java @@ -16,6 +16,8 @@ package com.android.emailcommon.mail; +import com.android.emailcommon.service.SearchParams; + public abstract class Folder { public enum OpenMode { @@ -107,21 +109,21 @@ public abstract class Folder { public abstract Message getMessage(String uid) throws MessagingException; - public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener) - throws MessagingException; - /** * Fetches the given list of messages. The specified listener is notified as * each fetch completes. Messages are downloaded as (as) lightweight (as * possible) objects to be filled in with later requests. In most cases this * means that only the UID is downloaded. */ - public abstract Message[] getMessages(MessageRetrievalListener listener) + public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener) + throws MessagingException; + + public abstract Message[] getMessages(SearchParams params,MessageRetrievalListener listener) throws MessagingException; public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException; - + /** * Return a set of messages based on the state of the flags. * Note: Not typically implemented in remote stores, so not abstract. diff --git a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java index 98d3f4dfe..9e292ce54 100644 --- a/emailcommon/src/com/android/emailcommon/provider/Mailbox.java +++ b/emailcommon/src/com/android/emailcommon/provider/Mailbox.java @@ -17,11 +17,6 @@ package com.android.emailcommon.provider; -import com.android.emailcommon.Logging; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.emailcommon.provider.EmailContent.SyncColumns; -import com.android.emailcommon.utility.Utility; - import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -31,6 +26,11 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.Log; +import com.android.emailcommon.Logging; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.EmailContent.SyncColumns; +import com.android.emailcommon.utility.Utility; + public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns, Parcelable { public static final String TABLE_NAME = "Mailbox"; @SuppressWarnings("hiding") @@ -390,6 +390,7 @@ public class Mailbox extends EmailContent implements SyncColumns, MailboxColumns } switch (getMailboxType(context, mailboxId)) { case -1: // not found + case TYPE_SEARCH: case TYPE_DRAFTS: case TYPE_OUTBOX: return false; diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 96174e0f9..98d6c561a 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -886,21 +886,50 @@ public class Controller { } /** - * Search for messages on the server; see {@Link EmailServiceProxy#searchMessages(long, long, - * boolean, String, int, int, long)} for a complete description of this method's arguments. + * Search for messages on the (IMAP) server; do not call this on the UI thread! + * @param accountId the id of the account to be searched + * @param searchParams the parameters for this search + * @throws MessagingException */ - public void searchMessages(final long accountId, final SearchParams searchParams, - final long destMailboxId) { + public void searchMessages(final long accountId, final SearchParams searchParams) + throws MessagingException { + // Find/create our search mailbox + Mailbox searchMailbox = getSearchMailbox(accountId); + if (searchMailbox == null) return; + final long searchMailboxId = searchMailbox.mId; + IEmailService service = getServiceForAccount(accountId); if (service != null) { // Service implementation try { - service.searchMessages(accountId, searchParams, destMailboxId); + service.searchMessages(accountId, searchParams, searchMailboxId); } catch (RemoteException e) { // TODO Change exception handling to be consistent with however this method // is implemented for other protocols Log.e("searchMessages", "RemoteException", e); } + } else { + // This is the actual mailbox we'll be searching + Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); + if (actualMailbox == null) return; + + // Delete existing contents of search mailbox + ContentResolver resolver = mContext.getContentResolver(); + resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailboxId, + null); + 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); + // Do the search + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); + } + mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); } } diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index 51f65f48e..72c591725 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -16,6 +16,15 @@ package com.android.email; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.os.Process; +import android.util.Log; + import com.android.email.mail.Sender; import com.android.email.mail.Store; import com.android.emailcommon.Logging; @@ -42,21 +51,15 @@ import com.android.emailcommon.provider.EmailContent.MailboxColumns; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.EmailContent.SyncColumns; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.SearchParams; import com.android.emailcommon.utility.AttachmentUtilities; import com.android.emailcommon.utility.ConversionUtilities; import com.android.emailcommon.utility.Utility; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.Uri; -import android.os.Process; -import android.util.Log; - import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -397,18 +400,268 @@ public class MessagingController implements Runnable { } } + /** + * Load the structure and body of messages not yet synced + * @param account the account we're syncing + * @param remoteFolder the (open) Folder we're working on + * @param unsyncedMessages an array of Message's we've got headers for + * @param toMailbox the destination mailbox we're syncing + * @throws MessagingException + */ + void loadUnsyncedMessages(final Account account, Folder remoteFolder, + ArrayList unsyncedMessages, final Mailbox toMailbox) + throws MessagingException { + + // 1. Divide the unsynced messages into small & large (by size) + + // TODO doing this work here (synchronously) is problematic because it prevents the UI + // from affecting the order (e.g. download a message because the user requested it.) Much + // of this logic should move out to a different sync loop that attempts to update small + // groups of messages at a time, as a background task. However, we can't just return + // (yet) because POP messages don't have an envelope yet.... + + ArrayList largeMessages = new ArrayList(); + ArrayList smallMessages = new ArrayList(); + for (Message message : unsyncedMessages) { + if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { + largeMessages.add(message); + } else { + smallMessages.add(message); + } + } + + // 2. Download small messages + + // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope, + // this is going to be inefficient and duplicate work we've already done. 2. It's going + // back to the DB for a local message that we already had (and discarded). + + // For small messages, we specify "body", which returns everything (incl. attachments) + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, + new MessageRetrievalListener() { + public void messageRetrieved(Message message) { + // Store the updated message locally and mark it fully loaded + copyOneMessageToProvider(message, account, toMailbox, + EmailContent.Message.FLAG_LOADED_COMPLETE); + } + + @Override + public void loadAttachmentProgress(int progress) { + } + }); + + // 3. Download large messages. We ask the server to give us the message structure, + // but not all of the attachments. + fp.clear(); + fp.add(FetchProfile.Item.STRUCTURE); + remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); + for (Message message : largeMessages) { + if (message.getBody() == null) { + // POP doesn't support STRUCTURE mode, so we'll just do a partial download + // (hopefully enough to see some/all of the body) and mark the message for + // further download. + fp.clear(); + fp.add(FetchProfile.Item.BODY_SANE); + // TODO a good optimization here would be to make sure that all Stores set + // the proper size after this fetch and compare the before and after size. If + // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED + remoteFolder.fetch(new Message[] { message }, fp, null); + + // Store the partially-loaded message and mark it partially loaded + copyOneMessageToProvider(message, account, toMailbox, + EmailContent.Message.FLAG_LOADED_PARTIAL); + } else { + // We have a structure to deal with, from which + // we can pull down the parts we want to actually store. + // Build a list of parts we are interested in. Text parts will be downloaded + // right now, attachments will be left for later. + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + // Download the viewables immediately + for (Part part : viewables) { + fp.clear(); + fp.add(part); + // TODO what happens if the network connection dies? We've got partial + // messages with incorrect status stored. + remoteFolder.fetch(new Message[] { message }, fp, null); + } + // Store the updated message locally and mark it fully loaded + copyOneMessageToProvider(message, account, toMailbox, + EmailContent.Message.FLAG_LOADED_COMPLETE); + } + } + + } + + public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, + Folder remoteFolder, ArrayList unsyncedMessages, + HashMap localMessageMap, final ArrayList unseenMessages) + throws MessagingException { + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + fp.add(FetchProfile.Item.ENVELOPE); + + final HashMap localMapCopy; + if (localMessageMap != null) + localMapCopy = new HashMap(localMessageMap); + else { + localMapCopy = new HashMap(); + } + + remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, + new MessageRetrievalListener() { + @Override + public void messageRetrieved(Message message) { + try { + // Determine if the new message was already known (e.g. partial) + // And create or reload the full message info + LocalMessageInfo localMessageInfo = + localMapCopy.get(message.getUid()); + EmailContent.Message localMessage = null; + if (localMessageInfo == null) { + localMessage = new EmailContent.Message(); + } else { + localMessage = EmailContent.Message.restoreMessageWithId( + mContext, localMessageInfo.mId); + } + + if (localMessage != null) { + try { + // Copy the fields that are available into the message + LegacyConversions.updateMessageFields(localMessage, + message, account.mId, mailbox.mId); + // Commit the message to the local store + saveOrUpdate(localMessage, mContext); + // Track the "new" ness of the downloaded message + if (!message.isSet(Flag.SEEN) && unseenMessages != null) { + unseenMessages.add(localMessage.mId); + } + } catch (MessagingException me) { + Log.e(Logging.LOG_TAG, + "Error while copying downloaded message." + me); + } + + } + } + catch (Exception e) { + Log.e(Logging.LOG_TAG, + "Error while storing downloaded message." + e.toString()); + } + } + + @Override + public void loadAttachmentProgress(int progress) { + } + }); + + } + + /** + * A message and numeric uid that's easily sortable + */ + private static class SortableMessage { + private final Message mMessage; + private final long mUid; + + SortableMessage(Message message, long uid) { + mMessage = message; + mUid = uid; + } + } + + public void searchMailbox(long accountId, SearchParams searchParams, final long destMailboxId) + throws MessagingException { + final Account account = Account.restoreAccountWithId(mContext, accountId); + final Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); + final Mailbox destMailbox = Mailbox.restoreMailboxWithId(mContext, destMailboxId); + if (account == null || mailbox == null || destMailbox == null) return; + + Store remoteStore = Store.getInstance(account, mContext, null); + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); + remoteFolder.open(OpenMode.READ_WRITE, null); + + // Get the "bare" messages (basically uid) + Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); + int remoteCount = remoteMessages.length; + if (remoteCount > 0) { + SortableMessage[] sortableMessages = new SortableMessage[remoteCount]; + int i = 0; + for (Message msg : remoteMessages) { + sortableMessages[i++] = new SortableMessage(msg, Long.parseLong(msg.getUid())); + } + // Sort the uid's, most recent first + // Note: Not all servers will be nice and return results in the order we request them; + // those that do will see messages arrive from newest to oldest (i.e. the "right" order) + Arrays.sort(sortableMessages, new Comparator() { + @Override + public int compare(SortableMessage lhs, SortableMessage rhs) { + return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0; + } + }); + final ArrayList messageList = new ArrayList(); + // Now create a list sized for our visible limit and fill it in + int messageListSize = Math.min(remoteCount, Email.VISIBLE_LIMIT_DEFAULT); + for (i = 0; i < messageListSize; i++) { + messageList.add(sortableMessages[i].mMessage); + } + // Get everything in one pass, rather than two (as in sync); this starts getting us + // usable results quickly. + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + fp.add(FetchProfile.Item.ENVELOPE); + fp.add(FetchProfile.Item.STRUCTURE); + fp.add(FetchProfile.Item.BODY_SANE); + remoteFolder.fetch(messageList.toArray(new Message[0]), fp, + new MessageRetrievalListener() { + public void messageRetrieved(Message message) { + try { + // Determine if the new message was already known (e.g. partial) + // And create or reload the full message info + EmailContent.Message localMessage = new EmailContent.Message(); + try { + // Copy the fields that are available into the message + LegacyConversions.updateMessageFields(localMessage, + message, account.mId, mailbox.mId); + // Commit the message to the local store + saveOrUpdate(localMessage, mContext); + localMessage.mMailboxKey = destMailboxId; + // We load 50k or so; maybe it's complete, maybe not... + int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; + if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { + flag = EmailContent.Message.FLAG_LOADED_PARTIAL; + } + copyOneMessageToProvider(message, localMessage, flag, mContext); + } catch (MessagingException me) { + Log.e(Logging.LOG_TAG, + "Error while copying downloaded message." + me); + } + } catch (Exception e) { + Log.e(Logging.LOG_TAG, + "Error while storing downloaded message." + e.toString()); + } + } + + @Override + public void loadAttachmentProgress(int progress) { + } + }); + } + } + /** * Generic synchronizer - used for POP3 and IMAP. * * TODO Break this method up into smaller chunks. * * @param account the account to sync - * @param folder the mailbox to sync + * @param mailbox the mailbox to sync * @return results of the sync pass * @throws MessagingException */ - private SyncResults synchronizeMailboxGeneric( - final Account account, final Mailbox folder) + private SyncResults synchronizeMailboxGeneric(final Account account, final Mailbox mailbox) throws MessagingException { /* @@ -421,8 +674,8 @@ public class MessagingController implements Runnable { ContentResolver resolver = mContext.getContentResolver(); // 0. We do not ever sync DRAFTS or OUTBOX (down or up) - if (folder.mType == Mailbox.TYPE_DRAFTS || folder.mType == Mailbox.TYPE_OUTBOX) { - int totalMessages = EmailContent.count(mContext, folder.getUri(), null, null); + if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { + int totalMessages = EmailContent.count(mContext, mailbox.getUri(), null, null); return new SyncResults(totalMessages, unseenMessages); } @@ -439,7 +692,7 @@ public class MessagingController implements Runnable { " AND " + MessageColumns.MAILBOX_KEY + "=?", new String[] { String.valueOf(account.mId), - String.valueOf(folder.mId) + String.valueOf(mailbox.mId) }, null); while (localUidCursor.moveToNext()) { @@ -452,20 +705,10 @@ public class MessagingController implements Runnable { } } - // 1a. Count the unread messages before changing anything - int localUnreadCount = EmailContent.count(mContext, EmailContent.Message.CONTENT_URI, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + - " AND " + MessageColumns.MAILBOX_KEY + "=?" + - " AND " + MessageColumns.FLAG_READ + "=0", - new String[] { - String.valueOf(account.mId), - String.valueOf(folder.mId) - }); - // 2. Open the remote folder and create the remote folder if necessary Store remoteStore = Store.getInstance(account, mContext, null); - Folder remoteFolder = remoteStore.getFolder(folder.mServerId); + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); /* * If the folder is a "special" folder we need to see if it exists @@ -474,8 +717,8 @@ public class MessagingController implements Runnable { * designed and on Imap folders during error conditions. This allows us * to treat Pop3 and Imap the same in this code. */ - if (folder.mType == Mailbox.TYPE_TRASH || folder.mType == Mailbox.TYPE_SENT - || folder.mType == Mailbox.TYPE_DRAFTS) { + if (mailbox.mType == Mailbox.TYPE_TRASH || mailbox.mType == Mailbox.TYPE_SENT + || mailbox.mType == Mailbox.TYPE_DRAFTS) { if (!remoteFolder.exists()) { if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { return new SyncResults(0, unseenMessages); @@ -493,7 +736,7 @@ public class MessagingController implements Runnable { int remoteMessageCount = remoteFolder.getMessageCount(); // 6. Determine the limit # of messages to download - int visibleLimit = folder.mVisibleLimit; + int visibleLimit = mailbox.mVisibleLimit; if (visibleLimit <= 0) { visibleLimit = Email.VISIBLE_LIMIT_DEFAULT; } @@ -548,56 +791,8 @@ public class MessagingController implements Runnable { * critical data as fast as possible, and then we'll fill in the details. */ if (unsyncedMessages.size() > 0) { - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - final HashMap localMapCopy = - new HashMap(localMessageMap); - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, - new MessageRetrievalListener() { - public void messageRetrieved(Message message) { - try { - // Determine if the new message was already known (e.g. partial) - // And create or reload the full message info - LocalMessageInfo localMessageInfo = - localMapCopy.get(message.getUid()); - EmailContent.Message localMessage = null; - if (localMessageInfo == null) { - localMessage = new EmailContent.Message(); - } else { - localMessage = EmailContent.Message.restoreMessageWithId( - mContext, localMessageInfo.mId); - } - - if (localMessage != null) { - try { - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, folder.mId); - // Commit the message to the local store - saveOrUpdate(localMessage, mContext); - // Track the "new" ness of the downloaded message - if (!message.isSet(Flag.SEEN)) { - unseenMessages.add(localMessage.mId); - } - } catch (MessagingException me) { - Log.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - - } - } - catch (Exception e) { - Log.e(Logging.LOG_TAG, - "Error while storing downloaded message." + e.toString()); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); + downloadFlagAndEnvelope(account, mailbox, remoteFolder, unsyncedMessages, + localMessageMap, unseenMessages); } // 9. Refresh the flags for any messages in the local store that we didn't just download. @@ -663,87 +858,7 @@ public class MessagingController implements Runnable { resolver.delete(deletERowToDelete, null, null); } - // 11. Divide the unsynced messages into small & large (by size) - - // TODO doing this work here (synchronously) is problematic because it prevents the UI - // from affecting the order (e.g. download a message because the user requested it.) Much - // of this logic should move out to a different sync loop that attempts to update small - // groups of messages at a time, as a background task. However, we can't just return - // (yet) because POP messages don't have an envelope yet.... - - ArrayList largeMessages = new ArrayList(); - ArrayList smallMessages = new ArrayList(); - for (Message message : unsyncedMessages) { - if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { - largeMessages.add(message); - } else { - smallMessages.add(message); - } - } - - // 12. Download small messages - - // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope, - // this is going to be inefficient and duplicate work we've already done. 2. It's going - // back to the DB for a local message that we already had (and discarded). - - // For small messages, we specify "body", which returns everything (incl. attachments) - fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, - new MessageRetrievalListener() { - public void messageRetrieved(Message message) { - // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(message, account, folder, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - // 13. Download large messages. We ask the server to give us the message structure, - // but not all of the attachments. - fp.clear(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); - for (Message message : largeMessages) { - if (message.getBody() == null) { - // POP doesn't support STRUCTURE mode, so we'll just do a partial download - // (hopefully enough to see some/all of the body) and mark the message for - // further download. - fp.clear(); - fp.add(FetchProfile.Item.BODY_SANE); - // TODO a good optimization here would be to make sure that all Stores set - // the proper size after this fetch and compare the before and after size. If - // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED - remoteFolder.fetch(new Message[] { message }, fp, null); - - // Store the partially-loaded message and mark it partially loaded - copyOneMessageToProvider(message, account, folder, - EmailContent.Message.FLAG_LOADED_PARTIAL); - } else { - // We have a structure to deal with, from which - // we can pull down the parts we want to actually store. - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - for (Part part : viewables) { - fp.clear(); - fp.add(part); - // TODO what happens if the network connection dies? We've got partial - // messages with incorrect status stored. - remoteFolder.fetch(new Message[] { message }, fp, null); - } - // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(message, account, folder, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - } + loadUnsyncedMessages(account, remoteFolder, unsyncedMessages, mailbox); // 14. Clean up and report results remoteFolder.close(false); diff --git a/src/com/android/email/activity/EmailActivity.java b/src/com/android/email/activity/EmailActivity.java index 7341621d5..0202c86ae 100644 --- a/src/com/android/email/activity/EmailActivity.java +++ b/src/com/android/email/activity/EmailActivity.java @@ -16,25 +16,10 @@ package com.android.email.activity; -import com.android.email.Controller; -import com.android.email.ControllerResultUiThreadWrapper; -import com.android.email.Email; -import com.android.email.MessagingExceptionStrings; -import com.android.email.R; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent.MailboxColumns; -import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.service.SearchParams; -import com.android.emailcommon.utility.EmailAsyncTask; - import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.Fragment; -import android.content.ContentResolver; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; @@ -49,6 +34,19 @@ import android.view.MenuItem; import android.view.View; import android.widget.TextView; +import com.android.email.Controller; +import com.android.email.ControllerResultUiThreadWrapper; +import com.android.email.Email; +import com.android.email.MessagingExceptionStrings; +import com.android.email.R; +import com.android.emailcommon.Logging; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.SearchParams; +import com.android.emailcommon.utility.EmailAsyncTask; + import java.util.ArrayList; /** @@ -264,32 +262,19 @@ public class EmailActivity extends Activity implements View.OnClickListener, Fra // Switch to search mailbox // TODO How to handle search from within the search mailbox?? final Controller controller = Controller.getInstance(mContext); - final Mailbox searchMailbox = controller.getSearchMailbox(accountId); - if (searchMailbox == null) return; - // Delete contents, add a placeholder - ContentResolver resolver = mContext.getContentResolver(); - resolver.delete(Message.CONTENT_URI, Message.MAILBOX_KEY + "=" + searchMailbox.mId, - null); - ContentValues cv = new ContentValues(); - cv.put(Mailbox.DISPLAY_NAME, queryString); - resolver.update(ContentUris.withAppendedId(Mailbox.CONTENT_URI, searchMailbox.mId), cv, - null, null); - Message msg = new Message(); - msg.mMailboxKey = searchMailbox.mId; - msg.mAccountKey = accountId; - msg.mDisplayName = "Searching for " + queryString; - msg.mTimeStamp = Long.MAX_VALUE; // Sort on top - msg.save(mContext); - - startActivity(createOpenMessageIntent(EmailActivity.this, - accountId, searchMailbox.mId, msg.mId)); EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - SearchParams searchSpec = new SearchParams(SearchParams.ALL_MAILBOXES, - queryString); - controller.searchMessages(accountId, searchSpec, searchMailbox.mId); + // TODO Use the proper parameters (a mailbox id for IMAP, a mailbox id or + // SearchParams.ALL_MAILBOXES for eas + // We assume IMAP here for testing + SearchParams searchSpec = new SearchParams(mailboxId, queryString); + try { + controller.searchMessages(accountId, searchSpec); + } catch (MessagingException e) { + // TODO What to do?? + } }}); return; } @@ -367,14 +352,28 @@ public class EmailActivity extends Activity implements View.OnClickListener, Fra public boolean onPrepareOptionsMenu(Menu menu) { // STOPSHIP Temporary sync options UI boolean isEas = false; + boolean canSearch = false; long accountId = mUIController.getActualAccountId(); if (accountId > 0) { // Move database operations out of the UI thread - isEas = "eas".equals(Account.getProtocol(mContext, accountId)); + if ("eas".equals(Account.getProtocol(mContext, accountId))) { + isEas = true; + Account account = Account.restoreAccountWithId(mContext, accountId); + if (account != null) { + // We should set a flag in the account indicating ability to handle search + String protocolVersion = account.mProtocolVersion; + if (Double.parseDouble(protocolVersion) >= 12.0) { + canSearch = true; + } + } + } else if ("imap".equals(Account.getProtocol(mContext, accountId))) { + canSearch = true; + } } // Should use an isSyncable call to prevent drafts/outbox from allowing this + menu.findItem(R.id.search).setVisible(canSearch); menu.findItem(R.id.sync_lookback).setVisible(isEas); menu.findItem(R.id.sync_frequency).setVisible(isEas); diff --git a/src/com/android/email/activity/UIControllerBase.java b/src/com/android/email/activity/UIControllerBase.java index 8eb1eeb7a..81ce33dea 100644 --- a/src/com/android/email/activity/UIControllerBase.java +++ b/src/com/android/email/activity/UIControllerBase.java @@ -16,16 +16,6 @@ package com.android.email.activity; -import com.android.email.Email; -import com.android.email.R; -import com.android.email.RefreshManager; -import com.android.email.activity.setup.AccountSettings; -import com.android.emailcommon.Logging; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent.Message; -import com.android.emailcommon.provider.Mailbox; -import com.android.emailcommon.utility.EmailAsyncTask; - import android.app.Activity; import android.app.Fragment; import android.app.FragmentManager; @@ -36,6 +26,16 @@ import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import com.android.email.Email; +import com.android.email.R; +import com.android.email.RefreshManager; +import com.android.email.activity.setup.AccountSettings; +import com.android.emailcommon.Logging; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.utility.EmailAsyncTask; + import java.util.LinkedList; import java.util.List; @@ -619,7 +619,8 @@ abstract class UIControllerBase implements MailboxListFragment.Callback, } } // Should use an isSearchable call to prevent search on inappropriate accounts/boxes - menu.findItem(R.id.search).setVisible(canSearch); + // STOPSHIP Figure out where the "canSearch" test belongs + menu.findItem(R.id.search).setVisible(true); //canSearch); return true; } diff --git a/src/com/android/email/mail/store/ImapFolder.java b/src/com/android/email/mail/store/ImapFolder.java index d2f85d9e2..26bcf3d7c 100644 --- a/src/com/android/email/mail/store/ImapFolder.java +++ b/src/com/android/email/mail/store/ImapFolder.java @@ -47,6 +47,7 @@ import com.android.emailcommon.mail.Message; import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.mail.Part; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.SearchParams; import com.android.emailcommon.utility.Utility; import java.io.IOException; @@ -404,6 +405,30 @@ class ImapFolder extends Folder { return null; } + /** + * Retrieve messages based on search parameters. We search FROM, TO, CC, SUBJECT, and BODY + * We send: SEARCH OR FROM "foo" (OR TO "foo" (OR CC "foo" (OR SUBJECT "foo" BODY "foo"))) + * TODO: Properly quote the filter + */ + @Override + public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) + throws MessagingException { + String filter = params.mFilter; + StringBuilder sb = new StringBuilder(); + sb.append("OR FROM \""); + sb.append(filter); + sb.append("\" (OR TO \""); + sb.append(filter); + sb.append("\" (OR CC \""); + sb.append(filter); + sb.append("\" (OR SUBJECT \""); + sb.append(filter); + sb.append("\" BODY \""); + sb.append(filter); + sb.append("\")))"); + return getMessagesInternal(searchForUids(sb.toString()), listener); + } + @Override public Message[] getMessages(int start, int end, MessageRetrievalListener listener) throws MessagingException { @@ -414,11 +439,6 @@ class ImapFolder extends Folder { searchForUids(String.format("%d:%d NOT DELETED", start, end)), listener); } - @Override - public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException { - return getMessages(null, listener); - } - @Override public Message[] getMessages(String[] uids, MessageRetrievalListener listener) throws MessagingException { @@ -576,11 +596,12 @@ class ImapFolder extends Folder { } if (fp.contains(FetchProfile.Item.BODY) || fp.contains(FetchProfile.Item.BODY_SANE)) { - // Body is keyed by "BODY[...". - // TOOD Should we accept "RFC822" as well?? - // The old code didn't really check the key, so it accepted any literal - // that first appeared. - ImapString body = fetchList.getKeyedStringOrEmpty("BODY[", true); + // Body is keyed by "BODY[]...". + // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." + // 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); } diff --git a/src/com/android/email/mail/store/Pop3Store.java b/src/com/android/email/mail/store/Pop3Store.java index aa661f674..45203a4df 100644 --- a/src/com/android/email/mail/store/Pop3Store.java +++ b/src/com/android/email/mail/store/Pop3Store.java @@ -16,6 +16,10 @@ package com.android.email.mail.store; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; + import com.android.email.Email; import com.android.email.mail.Store; import com.android.email.mail.Transport; @@ -26,20 +30,17 @@ import com.android.emailcommon.mail.AuthenticationFailedException; import com.android.emailcommon.mail.FetchProfile; import com.android.emailcommon.mail.Flag; import com.android.emailcommon.mail.Folder; +import com.android.emailcommon.mail.Folder.OpenMode; import com.android.emailcommon.mail.Message; import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.mail.Folder.OpenMode; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.service.EmailServiceProxy; +import com.android.emailcommon.service.SearchParams; import com.android.emailcommon.utility.LoggingInputStream; import com.android.emailcommon.utility.Utility; -import android.content.Context; -import android.os.Bundle; -import android.util.Log; - import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; @@ -607,12 +608,6 @@ public class Pop3Store extends Store { mUidToMsgNumMap.put(message.getUid(), msgNum); } - @Override - public Message[] getMessages(MessageRetrievalListener listener) { - throw new UnsupportedOperationException( - "Pop3Folder.getMessage(MessageRetrievalListener)"); - } - @Override public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { throw new UnsupportedOperationException( @@ -947,6 +942,12 @@ public class Pop3Store extends Store { public Message createMessage(String uid) { return new Pop3Message(uid, this); } + + @Override + public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) + throws MessagingException { + return null; + } } public static class Pop3Message extends MimeMessage { diff --git a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java index 8db8fcadf..4ed789d3f 100644 --- a/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/ImapStoreUnitTests.java @@ -16,6 +16,16 @@ package com.android.email.mail.store; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.SharedPreferences; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.test.InstrumentationTestCase; +import android.test.MoreAsserts; +import android.test.suitebuilder.annotation.SmallTest; + import com.android.email.DBTestHelper; import com.android.email.MockSharedPreferences; import com.android.email.MockVendorPolicy; @@ -49,16 +59,6 @@ import com.android.emailcommon.utility.Utility; import org.apache.commons.io.IOUtils; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.SharedPreferences; -import android.content.pm.PackageManager.NameNotFoundException; -import android.os.Build; -import android.os.Bundle; -import android.test.InstrumentationTestCase; -import android.test.MoreAsserts; -import android.test.suitebuilder.annotation.SmallTest; - import java.util.ArrayList; import java.util.HashMap; import java.util.regex.Pattern; @@ -1979,25 +1979,6 @@ public class ImapStoreUnitTests extends InstrumentationTestCase { mFolder.getMessages(new String[] {}, null)); } - /** - * Test for getMessages(MessageRetrievalListener), which is the same as - * getMessages(String[] uids, MessageRetrievalListener) where uids == null. - */ - public void testGetMessages3() throws Exception { - MockTransport mock = openAndInjectMockTransport(); - setupOpenFolder(mock); - mFolder.open(OpenMode.READ_WRITE, null); - - mock.expect( - getNextTag(false) + " UID SEARCH 1:\\* NOT DELETED", - new String[] { - "* sEARCH 3 4 5", - getNextTag(true) + " OK success" - }); - checkMessageUids(new String[] {"3", "4", "5"}, - mFolder.getMessages(null)); - } - private static void checkMessageUids(String[] expectedUids, Message[] actualMessages) { ArrayList list = new ArrayList(); for (Message m : actualMessages) { diff --git a/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java b/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java index ec6e9ea21..3823d8fc5 100644 --- a/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java +++ b/tests/src/com/android/email/mail/store/Pop3StoreUnitTests.java @@ -16,6 +16,9 @@ package com.android.email.mail.store; +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; + import com.android.email.mail.Transport; import com.android.email.mail.transport.MockTransport; import com.android.emailcommon.TempDirectory; @@ -32,9 +35,6 @@ import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; -import android.test.InstrumentationTestCase; -import android.test.suitebuilder.annotation.SmallTest; - /** * This is a series of unit tests for the POP3 Store class. These tests must be locally * complete - no server(s) required. @@ -283,22 +283,6 @@ public class Pop3StoreUnitTests extends InstrumentationTestCase { // getUnreadMessageCount() always returns -1 assertEquals(-1, mFolder.getUnreadMessageCount()); - // getMessages(MessageRetrievalListener listener) is unsupported - try { - mFolder.getMessages(null); - fail("Exception not thrown by getMessages()"); - } catch (UnsupportedOperationException e) { - // expected - succeed - } - - // getMessages(String[] uids, MessageRetrievalListener listener) is unsupported - try { - mFolder.getMessages(null, null); - fail("Exception not thrown by getMessages()"); - } catch (UnsupportedOperationException e) { - // expected - succeed - } - // getPermanentFlags() returns { Flag.DELETED } Flag[] flags = mFolder.getPermanentFlags(); assertEquals(1, flags.length); diff --git a/tests/src/com/android/emailcommon/mail/MockFolder.java b/tests/src/com/android/emailcommon/mail/MockFolder.java index d98c6e182..cf139ddeb 100644 --- a/tests/src/com/android/emailcommon/mail/MockFolder.java +++ b/tests/src/com/android/emailcommon/mail/MockFolder.java @@ -16,10 +16,8 @@ package com.android.emailcommon.mail; -import com.android.emailcommon.mail.FetchProfile; -import com.android.emailcommon.mail.Flag; -import com.android.emailcommon.mail.Folder; -import com.android.emailcommon.mail.Message; +import com.android.emailcommon.service.SearchParams; + public class MockFolder extends Folder { @@ -79,11 +77,6 @@ public class MockFolder extends Folder { return null; } - @Override - public Message[] getMessages(MessageRetrievalListener listener) { - return null; - } - @Override public Message[] getMessages(String[] uids, MessageRetrievalListener listener) { return null; @@ -127,4 +120,10 @@ public class MockFolder extends Folder { return null; } + @Override + public Message[] getMessages(SearchParams params, MessageRetrievalListener listener) + throws MessagingException { + return null; + } + }