First implementation of IMAP search

* Broke up synchronizeMailboxGeneric into three pieces; it's still
  horrible, but this at least stops my eyes from bleeding
* Remove unused method/tests from Folder interface

Change-Id: Ib4d979536be657137cf70ca535cf429d707be41b
This commit is contained in:
Marc Blank 2011-06-13 14:57:17 -07:00
parent 1579b7864a
commit 627bc6ed57
11 changed files with 435 additions and 302 deletions

View File

@ -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.

View File

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

View File

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

View File

@ -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<Message> 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<Message> largeMessages = new ArrayList<Message>();
ArrayList<Message> smallMessages = new ArrayList<Message>();
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<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
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<Message> unsyncedMessages,
HashMap<String, LocalMessageInfo> localMessageMap, final ArrayList<Long> unseenMessages)
throws MessagingException {
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.FLAGS);
fp.add(FetchProfile.Item.ENVELOPE);
final HashMap<String, LocalMessageInfo> localMapCopy;
if (localMessageMap != null)
localMapCopy = new HashMap<String, LocalMessageInfo>(localMessageMap);
else {
localMapCopy = new HashMap<String, LocalMessageInfo>();
}
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<SortableMessage>() {
@Override
public int compare(SortableMessage lhs, SortableMessage rhs) {
return lhs.mUid > rhs.mUid ? -1 : lhs.mUid < rhs.mUid ? 1 : 0;
}
});
final ArrayList<Message> messageList = new ArrayList<Message>();
// 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<String, LocalMessageInfo> localMapCopy =
new HashMap<String, LocalMessageInfo>(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<Message> largeMessages = new ArrayList<Message>();
ArrayList<Message> smallMessages = new ArrayList<Message>();
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<Part> viewables = new ArrayList<Part>();
ArrayList<Part> attachments = new ArrayList<Part>();
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);

View File

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

View File

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

View File

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

View File

@ -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 {

View File

@ -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<String> list = new ArrayList<String>();
for (Message m : actualMessages) {

View File

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

View File

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