From c84467afe1b5e0a657ed7d6a9fa1e3fe1ff259a0 Mon Sep 17 00:00:00 2001 From: Marc Blank Date: Wed, 8 Feb 2012 16:29:08 -0800 Subject: [PATCH] Start of IMAP conversion to Service architecture * Handle startSync and loadMore * Use SyncManager rather than MailService for periodic sync and upload sync * First of many CL's to disentangle sync from UI * Note that the large majority of this CL is a refactoring of IMAP specific code out of MessagingController and into ImapService; MessagingController will eventually be removed entirely from the app, as will much of Controller Change-Id: I13546d0694479b33cf93c25920dedc1d38227f6c --- AndroidManifest.xml | 11 + .../service/EmailServiceProxy.java | 61 +- .../service/IEmailServiceCallback.aidl | 8 + res/xml/syncadapter_pop_imap.xml | 3 +- src/com/android/email/Controller.java | 130 +- .../android/email/MessagingController.java | 807 -------- src/com/android/email/RefreshManager.java | 14 +- .../service/AttachmentDownloadService.java | 9 + .../email/service/EmailServiceUtils.java | 27 +- .../android/email/service/ImapService.java | 1717 +++++++++++++++++ .../android/email/service/MailService.java | 41 +- .../service/PopImapSyncAdapterService.java | 114 +- 12 files changed, 2023 insertions(+), 919 deletions(-) create mode 100644 src/com/android/email/service/ImapService.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index bf52778e5..8cdd3439c 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -395,6 +395,17 @@ + + + + + + _class, IEmailServiceCallback _callback) { super(_context, new Intent(_context, _class)); mCallback = _callback; + isRemote = false; } // The following two constructors are used with remote services that must be referenced by @@ -93,6 +96,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { } catch (IOException e) { } mCallback = _callback; + isRemote = true; } public EmailServiceProxy(Context _context, String _action, IEmailServiceCallback _callback) { @@ -102,6 +106,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { } catch (IOException e) { } mCallback = _callback; + isRemote = true; } @Override @@ -109,6 +114,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { mService = IEmailService.Stub.asInterface(binder); } + public boolean isRemote() { + return isRemote; + } + @Override public int getApiLevel() { return Api.LEVEL; @@ -124,9 +133,11 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param background whether or not this request corresponds to a background action (i.e. * prefetch) vs a foreground action (user request) */ + @Override public void loadAttachment(final long attachmentId, final boolean background) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { try { if (mCallback != null) mService.setCallback(mCallback); @@ -153,8 +164,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param mailboxId the id of the mailbox record * @param userRequest whether or not the user specifically asked for the sync */ + @Override public void startSync(final long mailboxId, final boolean userRequest) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { if (mCallback != null) mService.setCallback(mCallback); mService.startSync(mailboxId, userRequest); @@ -170,8 +183,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param mailboxId the id of the mailbox record * @param userRequest whether or not the user specifically asked for the sync */ + @Override public void stopSync(final long mailboxId) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { if (mCallback != null) mService.setCallback(mCallback); mService.stopSync(mailboxId); @@ -189,8 +204,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param hostAuth the hostauth object to validate * @return a Bundle as described above */ + @Override public Bundle validate(final HostAuth hostAuth) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException{ if (mCallback != null) mService.setCallback(mCallback); mReturn = mService.validate(hostAuth); @@ -219,9 +236,11 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param password the user's password * @return a Bundle as described above */ + @Override public Bundle autoDiscover(final String userName, final String password) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException{ if (mCallback != null) mService.setCallback(mCallback); mReturn = mService.autoDiscover(userName, password); @@ -244,8 +263,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * * @param accoundId the id of the account whose folder list is to be updated */ + @Override public void updateFolderList(final long accountId) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { if (mCallback != null) mService.setCallback(mCallback); mService.updateFolderList(accountId); @@ -259,8 +280,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * * @param flags an integer whose bits represent logging flags as defined in DEBUG_* flags above */ + @Override public void setLogging(final int flags) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { if (mCallback != null) mService.setCallback(mCallback); mService.setLogging(flags); @@ -274,8 +297,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * * @param cb a callback object through which all service callbacks are executed */ + @Override public void setCallback(final IEmailServiceCallback cb) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { mService.setCallback(cb); } @@ -289,8 +314,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * * @param accountId the id of the account whose host information has changed */ + @Override public void hostChanged(final long accountId) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { mService.hostChanged(accountId); } @@ -303,9 +330,11 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param messageId the id of the message containing the meeting request * @param response the response code, as defined in EmailServiceConstants */ + @Override public void sendMeetingResponse(final long messageId, final int response) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { if (mCallback != null) mService.setCallback(mCallback); mService.sendMeetingResponse(messageId, response); @@ -314,11 +343,19 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { } /** - * Not yet used; intended to request the sync adapter to load a complete message + * Request the sync adapter to load a complete message * * @param messageId the id of the message to be loaded */ - public void loadMore(long messageId) throws RemoteException { + @Override + public void loadMore(final long messageId) throws RemoteException { + setTask(new ProxyTask() { + @Override + public void run() throws RemoteException { + if (mCallback != null) mService.setCallback(mCallback); + mService.loadMore(messageId); + } + }, "startSync"); } /** @@ -327,6 +364,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param accountId the account in which the folder is to be created * @param name the name of the folder to be created */ + @Override public boolean createFolder(long accountId, String name) throws RemoteException { return false; } @@ -337,6 +375,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param accountId the account in which the folder resides * @param name the name of the folder to be deleted */ + @Override public boolean deleteFolder(long accountId, String name) throws RemoteException { return false; } @@ -348,6 +387,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param oldName the name of the existing folder * @param newName the new name for the folder */ + @Override public boolean renameFolder(long accountId, String oldName, String newName) throws RemoteException { return false; @@ -361,8 +401,10 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * * @param accountId the account whose data is to be deleted */ + @Override public void deleteAccountPIMData(final long accountId) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException { mService.deleteAccountPIMData(accountId); } @@ -385,9 +427,11 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { * @param destMailboxId the id of the mailbox into which search results are appended * @return the total number of matches for this search (regardless of how many were requested) */ + @Override public int searchMessages(final long accountId, final SearchParams searchParams, final long destMailboxId) throws RemoteException { setTask(new ProxyTask() { + @Override public void run() throws RemoteException{ if (mCallback != null) mService.setCallback(mCallback); mReturn = mService.searchMessages(accountId, searchParams, destMailboxId); @@ -400,6 +444,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService { return (Integer)mReturn; } } + @Override public IBinder asBinder() { return null; } diff --git a/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl b/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl index e4c6093fc..c713f5211 100644 --- a/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl +++ b/emailcommon/src/com/android/emailcommon/service/IEmailServiceCallback.aidl @@ -65,4 +65,12 @@ oneway interface IEmailServiceCallback { * progress = 0 for "start", 1..100 for optional progress reports */ void sendMessageStatus(long messageId, String subject, int statusCode, int progress); + + /** + * Callback to indicate that a particular message is being loaded + * messageId = the message being sent + * statusCode = 0 for OK, 1 for progress, other codes for error + * progress = 0 for "start", 1..100 for optional progress reports + */ + void loadMessageStatus(long messageId, int statusCode, int progress); } diff --git a/res/xml/syncadapter_pop_imap.xml b/res/xml/syncadapter_pop_imap.xml index c1c0734ea..f4a7120fc 100644 --- a/res/xml/syncadapter_pop_imap.xml +++ b/res/xml/syncadapter_pop_imap.xml @@ -23,5 +23,6 @@ diff --git a/src/com/android/email/Controller.java b/src/com/android/email/Controller.java index 93a7d160e..8a5f7cded 100644 --- a/src/com/android/email/Controller.java +++ b/src/com/android/email/Controller.java @@ -48,6 +48,7 @@ import com.android.emailcommon.provider.EmailContent.Message; import com.android.emailcommon.provider.EmailContent.MessageColumns; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.EmailServiceStatus; import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; @@ -339,6 +340,7 @@ public class Controller { /** * Request a remote update of mailboxes for an account. */ + @SuppressWarnings("deprecation") public void updateMailboxList(final long accountId) { Utility.runAsync(new Runnable() { @Override @@ -367,23 +369,15 @@ public class Controller { * Functionally this is quite similar to updateMailbox(), but it's a separate API and * separate callback in order to keep UI callbacks from affecting the service loop. */ + @SuppressWarnings("deprecation") public void serviceCheckMail(final long accountId, final long mailboxId, final long tag) { IEmailService service = getServiceForAccount(accountId); if (service != null) { - // Service implementation -// try { - // TODO this isn't quite going to work, because we're going to get the - // generic (UI) callbacks and not the ones we need to restart the ol' service. - // service.startSync(mailboxId, tag); mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); -// } catch (RemoteException e) { - // TODO Change exception handling to be consistent with however this method - // is implemented for other protocols -// Log.d("updateMailbox", "RemoteException" + e); -// } } else { // MessagingController implementation Utility.runAsync(new Runnable() { + @Override public void run() { mLegacyController.checkMail(accountId, tag, mLegacyListener); } @@ -398,6 +392,7 @@ public class Controller { * a simple message list. We should also at this point queue up a background task of * downloading some/all of the messages in this mailbox, but that should be interruptable. */ + @SuppressWarnings("deprecation") public void updateMailbox(final long accountId, final long mailboxId, boolean userRequest) { IEmailService service = getServiceForAccount(accountId); @@ -412,6 +407,7 @@ public class Controller { } else { // MessagingController implementation Utility.runAsync(new Runnable() { + @Override public void run() { // TODO shouldn't be passing fully-build accounts & mailboxes into APIs Account account = @@ -438,11 +434,13 @@ public class Controller { * @param messageId the message to load * @param callback the Controller callback by which results will be reported */ + @SuppressWarnings("deprecation") public void loadMessageForView(final long messageId) { // Split here for target type (Service or MessagingController) - IEmailService service = getServiceForMessage(messageId); - if (service != null) { + EmailServiceProxy service = getServiceForMessage(messageId); + if (service != null && service.isRemote()) { + // Get rid of this!! // There is no service implementation, so we'll just jam the value, log the error, // and get out of here. Uri uri = ContentUris.withAppendedId(Message.CONTENT_URI, messageId); @@ -456,9 +454,16 @@ public class Controller { listener.loadMessageForViewCallback(null, accountId, messageId, 100); } } + } else if (service != null) { + // IMAP here for now + try { + service.loadMore(messageId); + } catch (RemoteException e) { + } } else { // MessagingController implementation Utility.runAsync(new Runnable() { + @Override public void run() { mLegacyController.loadMessageForView(messageId, mLegacyListener); } @@ -593,6 +598,7 @@ public class Controller { sendPendingMessages(accountId); } + @SuppressWarnings("deprecation") private void sendPendingMessagesSmtp(long accountId) { // for IMAP & POP only, (attempt to) send the message now final Account account = @@ -602,6 +608,7 @@ public class Controller { } final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); Utility.runAsync(new Runnable() { + @Override public void run() { mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); } @@ -644,8 +651,10 @@ public class Controller { * look up limit * write limit into all mailboxes for that account */ + @SuppressWarnings("deprecation") public void resetVisibleLimits() { Utility.runAsync(new Runnable() { + @Override public void run() { ContentResolver resolver = mProviderContext.getContentResolver(); Cursor c = null; @@ -682,6 +691,7 @@ public class Controller { */ public void loadMoreMessages(final long mailboxId) { EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override public void run() { Mailbox mailbox = Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); if (mailbox == null) { @@ -746,6 +756,7 @@ public class Controller { */ public void deleteMessage(final long messageId) { EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override public void run() { deleteMessageSync(messageId); } @@ -760,6 +771,7 @@ public class Controller { throw new IllegalArgumentException(); } EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override public void run() { for (long messageId: messageIds) { deleteMessageSync(messageId); @@ -809,10 +821,6 @@ public class Controller { cv.put(EmailContent.MessageColumns.MAILBOX_KEY, trashMailboxId); resolver.update(uri, cv, null, null); } - - if (isMessagingController(account)) { - mLegacyController.processPendingActions(account.mId); - } } /** @@ -834,6 +842,7 @@ public class Controller { throw new IllegalArgumentException(); } return EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override public void run() { Account account = Account.getAccountForMessageId(mProviderContext, messageIds[0]); if (account != null) { @@ -845,9 +854,6 @@ public class Controller { EmailContent.Message.SYNCED_CONTENT_URI, messageId); resolver.update(uri, cv, null, null); } - if (isMessagingController(account)) { - mLegacyController.processPendingActions(account.mId); - } } } }); @@ -888,13 +894,6 @@ public class Controller { private void updateMessageSync(long messageId, ContentValues cv) { Uri uri = ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); mProviderContext.getContentResolver().update(uri, cv, null, null); - - // Service runs automatically, MessagingController needs a kick - long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); - if (accountId == Account.NO_ACCOUNT) return; - if (isMessagingController(accountId)) { - mLegacyController.processPendingActions(accountId); - } } /** @@ -906,6 +905,7 @@ public class Controller { public void setMessageAnsweredOrForwarded(final long messageId, final int flag) { EmailAsyncTask.runAsyncParallel(new Runnable() { + @Override public void run() { Message msg = Message.restoreMessageWithId(mProviderContext, messageId); if (msg == null) { @@ -1019,7 +1019,9 @@ public class Controller { if (Email.DEBUG) { Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); } - return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); + // Plumb this + return 0; + //return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); } } @@ -1085,7 +1087,7 @@ public class Controller { * @param messageId the message of interest * @result service proxy, or null if n/a */ - private IEmailService getServiceForMessage(long messageId) { + private EmailServiceProxy getServiceForMessage(long messageId) { // TODO make this more efficient, caching the account, smaller lookup here, etc. Message message = Message.restoreMessageWithId(mProviderContext, messageId); if (message == null) { @@ -1100,15 +1102,22 @@ public class Controller { * @param accountId the message of interest * @result service proxy, or null if n/a */ - private IEmailService getServiceForAccount(long accountId) { + private EmailServiceProxy getServiceForAccount(long accountId) { if (isMessagingController(accountId)) return null; + if (Account.getProtocol(mContext, accountId).equals(HostAuth.SCHEME_IMAP)) { + return getImapEmailService(); + } return getExchangeEmailService(); } - private IEmailService getExchangeEmailService() { + private EmailServiceProxy getExchangeEmailService() { return EmailServiceUtils.getExchangeService(mContext, mServiceCallback); } + private EmailServiceProxy getImapEmailService() { + return EmailServiceUtils.getImapService(mContext, mServiceCallback); + } + /** * Simple helper to determine if legacy MessagingController should be used */ @@ -1121,7 +1130,7 @@ public class Controller { Boolean isLegacyController = mLegacyControllerMap.get(accountId); if (isLegacyController == null) { String protocol = Account.getProtocol(mProviderContext, accountId); - isLegacyController = ("pop3".equals(protocol) || "imap".equals(protocol)); + isLegacyController = (HostAuth.SCHEME_POP3.equals(protocol)); mLegacyControllerMap.put(accountId, isLegacyController); } return isLegacyController; @@ -1581,8 +1590,7 @@ public class Controller { */ private class ServiceCallback extends IEmailServiceCallback.Stub { - private final static boolean DEBUG_FAIL_DOWNLOADS = false; // do not check in "true" - + @Override public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress) { MessagingException result = mapStatusToException(statusCode); @@ -1591,10 +1599,6 @@ public class Controller { progress = 100; break; case EmailServiceStatus.IN_PROGRESS: - if (DEBUG_FAIL_DOWNLOADS && progress > 75) { - result = new MessagingException( - String.valueOf(EmailServiceStatus.CONNECTION_ERROR)); - } // discard progress reports that look like sentinels if (progress < 0 || progress >= 100) { return; @@ -1616,6 +1620,7 @@ public class Controller { * However, this is sufficient for basic "progress=100" notification that message send * has just completed. */ + @Override public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) { long accountId = -1; // This should be in the callback @@ -1638,6 +1643,35 @@ public class Controller { } } + /** + * Note, this is an incomplete implementation of this callback, because we are + * not getting things back from Service in quite the same way as from MessagingController. + * However, this is sufficient for basic "progress=100" notification that message send + * has just completed. + */ + @Override + public void loadMessageStatus(long messageId, int statusCode, int progress) { + long accountId = -1; // This should be in the callback + MessagingException result = mapStatusToException(statusCode); + switch (statusCode) { + case EmailServiceStatus.SUCCESS: + progress = 100; + break; + case EmailServiceStatus.IN_PROGRESS: + // discard progress reports that look like sentinels + if (progress < 0 || progress >= 100) { + return; + } + break; + } + synchronized(mListeners) { + for (Result listener : mListeners) { + listener.loadMessageForViewCallback(result, accountId, messageId, progress); + } + } + } + + @Override public void syncMailboxListStatus(long accountId, int statusCode, int progress) { MessagingException result = mapStatusToException(statusCode); switch (statusCode) { @@ -1658,6 +1692,7 @@ public class Controller { } } + @Override public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { MessagingException result = mapStatusToException(statusCode); switch (statusCode) { @@ -1752,6 +1787,7 @@ public class Controller { } } + @Override public void loadAttachmentStatus(final long messageId, final long attachmentId, final int status, final int progress) { broadcastCallback(new ServiceCallbackWrapper() { @@ -1766,6 +1802,10 @@ public class Controller { public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ } + @Override + public void loadMessageStatus(long messageId, int statusCode, int progress){ + } + @Override public void syncMailboxListStatus(long accountId, int statusCode, int progress) { } @@ -1782,20 +1822,25 @@ public class Controller { */ private final IEmailService.Stub mBinder = new IEmailService.Stub() { + @Override public Bundle validate(HostAuth hostAuth) { return null; } + @Override public Bundle autoDiscover(String userName, String password) { return null; } + @Override public void startSync(long mailboxId, boolean userRequest) { } + @Override public void stopSync(long mailboxId) { } + @Override public void loadAttachment(long attachmentId, boolean background) throws RemoteException { Attachment att = Attachment.restoreAttachmentWithId(ControllerService.this, @@ -1835,41 +1880,52 @@ public class Controller { } } + @Override public void updateFolderList(long accountId) { } + @Override public void hostChanged(long accountId) { } + @Override public void setLogging(int flags) { } + @Override public void sendMeetingResponse(long messageId, int response) { } + @Override public void loadMore(long messageId) { } // The following three methods are not implemented in this version + @Override public boolean createFolder(long accountId, String name) { return false; } + @Override public boolean deleteFolder(long accountId, String name) { return false; } + @Override public boolean renameFolder(long accountId, String oldName, String newName) { return false; } + @Override public void setCallback(IEmailServiceCallback cb) { sCallbackList.register(cb); } + @Override public void deleteAccountPIMData(long accountId) { } + @Override public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { return 0; diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java index a123c24b2..27c5eee45 100644 --- a/src/com/android/email/MessagingController.java +++ b/src/com/android/email/MessagingController.java @@ -41,7 +41,6 @@ import com.android.emailcommon.mail.Flag; import com.android.emailcommon.mail.Folder; import com.android.emailcommon.mail.Folder.FolderType; import com.android.emailcommon.mail.Folder.MessageRetrievalListener; -import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; import com.android.emailcommon.mail.Folder.OpenMode; import com.android.emailcommon.mail.Message; import com.android.emailcommon.mail.MessagingException; @@ -54,15 +53,12 @@ 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 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; @@ -103,23 +99,11 @@ public class MessagingController implements Runnable { */ private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); - private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; - private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; - private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; - /** * We write this into the serverId field of messages that will never be upsynced. */ private static final String LOCAL_SERVERID_PREFIX = "Local-"; - /** - * Cache search results by account; this allows for "load more" support without having to - * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory - * shouldn't be an issue - */ - private static final HashMap sSearchResults = - new HashMap(); - private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues(); static { PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI); @@ -363,7 +347,6 @@ public class MessagingController implements Runnable { } NotificationController nc = NotificationController.getInstance(mContext); try { - processPendingActionsSynchronous(account); // Select generic sync or store-specific sync SyncResults results = synchronizeMailboxGeneric(account, folder); @@ -592,135 +575,6 @@ public class MessagingController implements Runnable { } - /** - * 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 int searchMailbox(long accountId, SearchParams searchParams, long destMailboxId) - throws MessagingException { - try { - return searchMailboxImpl(accountId, searchParams, destMailboxId); - } finally { - // Tell UI that we're done loading any search results (no harm calling this even if we - // encountered an error or never sent a "started" message) - mListeners.synchronizeMailboxFinished(accountId, destMailboxId, 0, 0, null); - } - } - - private int searchMailboxImpl(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) { - Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams - + " but account or mailbox information was missing"); - return 0; - } - - // Tell UI that we're loading messages - mListeners.synchronizeMailboxStarted(accountId, destMailbox.mId); - - Store remoteStore = Store.getInstance(account, mContext); - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - remoteFolder.open(OpenMode.READ_WRITE); - - SortableMessage[] sortableMessages = new SortableMessage[0]; - if (searchParams.mOffset == 0) { - // Get the "bare" messages (basically uid) - Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); - int remoteCount = remoteMessages.length; - if (remoteCount > 0) { - 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 of request; - // those that do will see messages arrive from newest to oldest - 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; - } - }); - sSearchResults.put(accountId, sortableMessages); - } - } else { - sortableMessages = sSearchResults.get(accountId); - } - - final int numSearchResults = sortableMessages.length; - final int numToLoad = - Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); - if (numToLoad <= 0) { - return 0; - } - - final ArrayList messageList = new ArrayList(); - for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; 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() { - @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 - 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; - // 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; - } - 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) { - } - }); - return numSearchResults; - } - - /** * Generic synchronizer - used for POP3 and IMAP. * @@ -1053,667 +907,6 @@ public class MessagingController implements Runnable { } } - public void processPendingActions(final long accountId) { - put("processPendingActions", null, new Runnable() { - @Override - public void run() { - try { - Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) { - return; - } - processPendingActionsSynchronous(account); - } - catch (MessagingException me) { - if (Logging.LOGD) { - Log.v(Logging.LOG_TAG, "processPendingActions", me); - } - /* - * Ignore any exceptions from the commands. Commands will be processed - * on the next round. - */ - } - } - }); - } - - /** - * Find messages in the updated table that need to be written back to server. - * - * Handles: - * Read/Unread - * Flagged - * Append (upload) - * Move To Trash - * Empty trash - * TODO: - * Move - * - * @param account the account to scan for pending actions - * @throws MessagingException - */ - private void processPendingActionsSynchronous(Account account) - throws MessagingException { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); - ContentResolver resolver = mContext.getContentResolver(); - String[] accountIdArgs = new String[] { Long.toString(account.mId) }; - - // Handle deletes first, it's always better to get rid of things first - processPendingDeletesSynchronous(account, resolver, accountIdArgs); - - // Handle uploads (currently, only to sent messages) - processPendingUploadsSynchronous(account, resolver, accountIdArgs); - - // Now handle updates / upsyncs - 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. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private void processPendingDeletesSynchronous(Account account, - ContentResolver resolver, String[] accountIdArgs) { - Cursor deletes = resolver.query(EmailContent.Message.DELETED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - try { - // Defer setting up the store until we know we need to access it - Store remoteStore = null; - // loop through messages marked as deleted - while (deletes.moveToNext()) { - boolean deleteFromTrash = false; - - EmailContent.Message oldMessage = - EmailContent.getContent(deletes, EmailContent.Message.class); - - if (oldMessage != null) { - lastMessageId = oldMessage.mId; - - 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); - } - - // Dispatch here for specific change types - if (deleteFromTrash) { - // Move message to trash - processPendingDeleteFromTrash(remoteStore, account, mailbox, oldMessage); - } - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, - 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. - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending delete for id=" - + lastMessageId + ": " + me); - } - } finally { - deletes.close(); - } - } - - /** - * Scan for messages that are in Sent, and are in need of upload, - * and send them to the server. "In need of upload" is defined as: - * serverId == null (no UID has been assigned) - * or - * message is in the updated list - * - * Note we also look for messages that are moving from drafts->outbox->sent. They never - * go through "drafts" or "outbox" on the server, so we hang onto these until they can be - * uploaded directly to the Sent folder. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private void processPendingUploadsSynchronous(Account account, - ContentResolver resolver, String[] accountIdArgs) { - // Find the Sent folder (since that's all we're uploading for now - Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, - MailboxColumns.ACCOUNT_KEY + "=?" - + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT, - accountIdArgs, null); - long lastMessageId = -1; - try { - // Defer setting up the store until we know we need to access it - Store remoteStore = null; - while (mailboxes.moveToNext()) { - long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN); - String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) }; - // Demand load mailbox - Mailbox mailbox = null; - - // First handle the "new" messages (serverId == null) - Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI, - EmailContent.Message.ID_PROJECTION, - EmailContent.Message.MAILBOX_KEY + "=?" - + " and (" + EmailContent.Message.SERVER_ID + " is null" - + " or " + EmailContent.Message.SERVER_ID + "=''" + ")", - mailboxKeyArgs, - null); - try { - while (upsyncs1.moveToNext()) { - // Load the remote store if it will be needed - if (remoteStore == null) { - remoteStore = Store.getInstance(account, mContext); - } - // Load the mailbox if it will be needed - if (mailbox == null) { - mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - } - // upsync the message - long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); - lastMessageId = id; - processUploadMessage(resolver, remoteStore, account, mailbox, id); - } - } finally { - if (upsyncs1 != null) { - upsyncs1.close(); - } - } - - // Next, handle any updates (e.g. edited in place, although this shouldn't happen) - Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.ID_PROJECTION, - EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs, - null); - try { - while (upsyncs2.moveToNext()) { - // Load the remote store if it will be needed - if (remoteStore == null) { - remoteStore = Store.getInstance(account, mContext); - } - // Load the mailbox if it will be needed - if (mailbox == null) { - mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); - if (mailbox == null) { - continue; // Mailbox removed. Move to the next message. - } - } - // upsync the message - long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); - lastMessageId = id; - processUploadMessage(resolver, remoteStore, account, mailbox, id); - } - } finally { - if (upsyncs2 != null) { - upsyncs2.close(); - } - } - } - } 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. - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id=" - + lastMessageId + ": " + me); - } - } finally { - if (mailboxes != null) { - mailboxes.close(); - } - } - } - - /** - * Scan for messages that are in the Message_Updates table, look for differences that - * we can deal with, and do the work. - * - * @param account - * @param resolver - * @param accountIdArgs - */ - private void processPendingUpdatesSynchronous(Account account, - ContentResolver resolver, String[] accountIdArgs) { - Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, - EmailContent.Message.CONTENT_PROJECTION, - EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, - EmailContent.MessageColumns.MAILBOX_KEY); - long lastMessageId = -1; - 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 needing updates - while (updates.moveToNext()) { - boolean changeMoveToTrash = false; - boolean changeRead = false; - boolean changeFlagged = false; - boolean changeMailbox = false; - boolean changeAnswered = false; - - EmailContent.Message oldMessage = - EmailContent.getContent(updates, EmailContent.Message.class); - lastMessageId = oldMessage.mId; - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(mContext, oldMessage.mId); - if (newMessage != null) { - 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) { - changeMoveToTrash = true; - } else { - changeMailbox = true; - } - } - changeRead = oldMessage.mFlagRead != newMessage.mFlagRead; - changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite; - changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO); - } - - // Load the remote store if it will be needed - if (remoteStore == null && - (changeMoveToTrash || changeRead || changeFlagged || changeMailbox || - changeAnswered)) { - remoteStore = Store.getInstance(account, mContext); - } - - // Dispatch here for specific change types - if (changeMoveToTrash) { - // Move message to trash - processPendingMoveToTrash(remoteStore, account, mailbox, oldMessage, - newMessage); - } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) { - processPendingDataChange(remoteStore, mailbox, changeRead, changeFlagged, - changeMailbox, changeAnswered, oldMessage, newMessage); - } - - // Finally, delete the update - Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, - 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. - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Unable to process pending update for id=" - + lastMessageId + ": " + me); - } - } finally { - updates.close(); - } - } - - /** - * Upsync an entire message. This must also unwind whatever triggered it (either by - * updating the serverId, or by deleting the update record, or it's going to keep happening - * over and over again. - * - * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload. - * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select - * only the Drafts and Sent folders, this can happen when the update record and the current - * record mismatch. In this case, we let the update record remain, because the filters - * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it) - * appropriately. - * - * @param resolver - * @param remoteStore - * @param account - * @param mailbox the actual mailbox - * @param messageId - */ - private void processUploadMessage(ContentResolver resolver, Store remoteStore, - Account account, Mailbox mailbox, long messageId) - throws MessagingException { - EmailContent.Message newMessage = - EmailContent.Message.restoreMessageWithId(mContext, messageId); - boolean deleteUpdate = false; - if (newMessage == null) { - deleteUpdate = true; - Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); - } else if (mailbox.mType == Mailbox.TYPE_TRASH) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); - } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) { - deleteUpdate = false; - Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); - } else { - Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId); - deleteUpdate = processPendingAppend(remoteStore, account, mailbox, newMessage); - } - if (deleteUpdate) { - // Finally, delete the update (if any) - Uri uri = ContentUris.withAppendedId( - EmailContent.Message.UPDATED_CONTENT_URI, messageId); - resolver.delete(uri, null, null); - } - } - - /** - * Upsync changes to read, flagged, or mailbox - * - * @param remoteStore the remote store for this mailbox - * @param mailbox the mailbox the message is stored in - * @param changeRead whether the message's read state has changed - * @param changeFlagged whether the message's flagged state has changed - * @param changeMailbox whether the message's mailbox has changed - * @param oldMessage the message in it's pre-change state - * @param newMessage the current version of the message - */ - private void processPendingDataChange(Store remoteStore, Mailbox mailbox, boolean changeRead, - boolean changeFlagged, boolean changeMailbox, boolean changeAnswered, - EmailContent.Message oldMessage, final EmailContent.Message newMessage) - throws MessagingException { - // 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("") - || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) { - return; - } - - // 1. No remote update for DRAFTS or OUTBOX - if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { - return; - } - - // 2. Open the remote store & folder - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return; - } - - // 3. Finally, apply the changes to the message - Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); - if (remoteMessage == null) { - return; - } - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, - "Update for msg id=" + newMessage.mId - + " read=" + newMessage.mFlagRead - + " flagged=" + newMessage.mFlagFavorite - + " answered=" - + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0) - + " new mailbox=" + newMessage.mMailboxKey); - } - Message[] messages = new Message[] { remoteMessage }; - if (changeRead) { - remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead); - } - if (changeFlagged) { - remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite); - } - if (changeAnswered) { - remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED, - (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0); - } - if (changeMailbox) { - Folder toFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - // We may need the message id to search for the message in the destination folder - remoteMessage.setMessageId(newMessage.mMessageId); - // Copy the message to its new folder - remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, newUid); - // We only have one message, so, any updates _must_ be for it. Otherwise, - // we'd have to cycle through to find the one with the same server ID. - mContext.getContentResolver().update(ContentUris.withAppendedId( - EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null); - } - @Override - public void onMessageNotFound(Message message) { - } - }); - // Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - } - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param account The account in which we are working - * @param newMailbox The local trash mailbox - * @param oldMessage The message copy that was saved in the updates shadow table - * @param newMessage The message that was moved to the mailbox - */ - private void processPendingMoveToTrash(Store remoteStore, - Account account, Mailbox newMailbox, EmailContent.Message oldMessage, - final EmailContent.Message newMessage) throws MessagingException { - - // 0. No remote move if the message is local-only - if (newMessage.mServerId == null || newMessage.mServerId.equals("") - || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { - return; - } - - // 1. Escape early if we can't find the local mailbox - // TODO smaller projection here - Mailbox oldMailbox = getRemoteMailboxForMessage(oldMessage); - if (oldMailbox == null) { - // can't find old mailbox, it may have been deleted. just return. - return; - } - // 2. We don't support delete-from-trash here - if (oldMailbox.mType == Mailbox.TYPE_TRASH) { - return; - } - - // 3. If DELETE_POLICY_NEVER, simply write back the deleted sentinel and return - // - // This sentinel takes the place of the server-side message, and locally "deletes" it - // by inhibiting future sync or display of the message. It will eventually go out of - // scope when it becomes old, or is deleted on the server, and the regular sync code - // will clean it up for us. - if (account.getDeletePolicy() == Account.DELETE_POLICY_NEVER) { - EmailContent.Message sentinel = new EmailContent.Message(); - sentinel.mAccountKey = oldMessage.mAccountKey; - sentinel.mMailboxKey = oldMessage.mMailboxKey; - sentinel.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED; - sentinel.mFlagRead = true; - sentinel.mServerId = oldMessage.mServerId; - sentinel.save(mContext); - - return; - } - - // The rest of this method handles server-side deletion - - // 4. Find the remote mailbox (that we deleted from), and open it - Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteFolder.exists()) { - return; - } - - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - return; - } - - // 5. Find the remote original message - Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteFolder.close(false); - return; - } - - // 6. Find the remote trash folder, and create it if not found - Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - /* - * If the remote trash folder doesn't exist we try to create it. - */ - remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); - } - - // 7. Try to copy the message into the remote trash folder - // Note, this entire section will be skipped for POP3 because there's no remote trash - if (remoteTrashFolder.exists()) { - /* - * Because remoteTrashFolder may be new, we need to explicitly open it - */ - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteFolder.close(false); - remoteTrashFolder.close(false); - return; - } - - remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, - new Folder.MessageUpdateCallbacks() { - @Override - public void onMessageUidChange(Message message, String newUid) { - // update the UID in the local trash folder, because some stores will - // have to change it when copying to remoteTrashFolder - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, newUid); - mContext.getContentResolver().update(newMessage.getUri(), cv, null, null); - } - - /** - * This will be called if the deleted message doesn't exist and can't be - * deleted (e.g. it was already deleted from the server.) In this case, - * attempt to delete the local copy as well. - */ - @Override - public void onMessageNotFound(Message message) { - mContext.getContentResolver().delete(newMessage.getUri(), null, null); - } - }); - remoteTrashFolder.close(false); - } - - // 8. Delete the message from the remote source folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteFolder.expunge(); - remoteFolder.close(false); - } - - /** - * Process a pending trash message command. - * - * @param remoteStore the remote store we're working in - * @param account The account in which we are working - * @param oldMailbox The local trash mailbox - * @param oldMessage The message that was deleted from the trash - */ - private void processPendingDeleteFromTrash(Store remoteStore, - Account account, Mailbox oldMailbox, EmailContent.Message oldMessage) - throws MessagingException { - - // 1. We only support delete-from-trash here - if (oldMailbox.mType != Mailbox.TYPE_TRASH) { - return; - } - - // 2. Find the remote trash folder (that we are deleting from), and open it - Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId); - if (!remoteTrashFolder.exists()) { - return; - } - - remoteTrashFolder.open(OpenMode.READ_WRITE); - if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { - remoteTrashFolder.close(false); - return; - } - - // 3. Find the remote original message - Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId); - if (remoteMessage == null) { - remoteTrashFolder.close(false); - return; - } - - // 4. Delete the message from the remote trash folder - remoteMessage.setFlag(Flag.DELETED, true); - remoteTrashFolder.expunge(); - remoteTrashFolder.close(false); - } - /** * Process a pending append message command. This command uploads a local message to the * server, first checking to be sure that the server message is not newer than diff --git a/src/com/android/email/RefreshManager.java b/src/com/android/email/RefreshManager.java index 4e5fd5876..2ba90dc96 100644 --- a/src/com/android/email/RefreshManager.java +++ b/src/com/android/email/RefreshManager.java @@ -16,15 +16,15 @@ package com.android.email; -import com.android.emailcommon.Logging; -import com.android.emailcommon.mail.MessagingException; -import com.android.emailcommon.utility.Utility; - import android.content.Context; import android.os.AsyncTask; import android.os.Handler; import android.util.Log; +import com.android.emailcommon.Logging; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.utility.Utility; + import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; @@ -107,11 +107,13 @@ public class RefreshManager { private long mLastRefreshTime; public boolean isRefreshing() { - return mIsRefreshRequested || mIsRefreshing; + // NOTE: For now, we're always allowing refresh (during service refactor) + return false; //return mIsRefreshRequested || mIsRefreshing; } public boolean canRefresh() { - return !isRefreshing(); + // NOTE: For now, we're always allowing refresh (during service refactor) + return true; //return !isRefreshing(); } public void onRefreshRequested() { diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index 18468fbb1..b2defe52d 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -155,6 +155,7 @@ public class AttachmentDownloadService extends Service implements Runnable { @Override public void onReceive(final Context context, Intent intent) { new Thread(new Runnable() { + @Override public void run() { watchdogAlarm(); } @@ -652,6 +653,7 @@ public class AttachmentDownloadService extends Service implements Runnable { * single callback that's defined by the EmailServiceCallback interface. */ private class ServiceCallback extends IEmailServiceCallback.Stub { + @Override public void loadAttachmentStatus(long messageId, long attachmentId, int statusCode, int progress) { // Record status and progress @@ -697,6 +699,11 @@ public class AttachmentDownloadService extends Service implements Runnable { public void syncMailboxStatus(long mailboxId, int statusCode, int progress) throws RemoteException { } + + @Override + public void loadMessageStatus(long messageId, int statusCode, int progress) + throws RemoteException { + } } /** @@ -801,6 +808,7 @@ public class AttachmentDownloadService extends Service implements Runnable { */ public static void attachmentChanged(final Context context, final long id, final int flags) { Utility.runAsync(new Runnable() { + @Override public void run() { Attachment attachment = Attachment.restoreAttachmentWithId(context, id); if (attachment != null) { @@ -867,6 +875,7 @@ public class AttachmentDownloadService extends Service implements Runnable { } } + @Override public void run() { // These fields are only used within the service thread mContext = this; diff --git a/src/com/android/email/service/EmailServiceUtils.java b/src/com/android/email/service/EmailServiceUtils.java index 38e35195d..d76325b2d 100644 --- a/src/com/android/email/service/EmailServiceUtils.java +++ b/src/com/android/email/service/EmailServiceUtils.java @@ -48,7 +48,7 @@ public class EmailServiceUtils { * @param context * @param callback Object to get callback, or can be null */ - public static IEmailService getService(Context context, String intentAction, + public static EmailServiceProxy getService(Context context, String intentAction, IEmailServiceCallback callback) { return new EmailServiceProxy(context, intentAction, callback); } @@ -64,11 +64,16 @@ public class EmailServiceUtils { startService(context, EmailServiceProxy.EXCHANGE_INTENT); } - public static IEmailService getExchangeService(Context context, + public static EmailServiceProxy getExchangeService(Context context, IEmailServiceCallback callback) { return getService(context, EmailServiceProxy.EXCHANGE_INTENT, callback); } + public static EmailServiceProxy getImapService(Context context, + IEmailServiceCallback callback) { + return new EmailServiceProxy(context, ImapService.class, callback); + } + public static boolean isExchangeAvailable(Context context) { return isServiceAvailable(context, EmailServiceProxy.EXCHANGE_INTENT); } @@ -85,65 +90,83 @@ public class EmailServiceUtils { public static class NullEmailService extends Service implements IEmailService { public static final NullEmailService INSTANCE = new NullEmailService(); + @Override public int getApiLevel() { return Api.LEVEL; } + @Override public Bundle autoDiscover(String userName, String password) throws RemoteException { return Bundle.EMPTY; } + @Override public boolean createFolder(long accountId, String name) throws RemoteException { return false; } + @Override public boolean deleteFolder(long accountId, String name) throws RemoteException { return false; } + @Override public void hostChanged(long accountId) throws RemoteException { } + @Override public void loadAttachment(long attachmentId, boolean background) throws RemoteException { } + @Override public void loadMore(long messageId) throws RemoteException { } + @Override public boolean renameFolder(long accountId, String oldName, String newName) throws RemoteException { return false; } + @Override public void sendMeetingResponse(long messageId, int response) throws RemoteException { } + @Override public void setCallback(IEmailServiceCallback cb) throws RemoteException { } + @Override public void setLogging(int flags) throws RemoteException { } + @Override public void startSync(long mailboxId, boolean userRequest) throws RemoteException { } + @Override public void stopSync(long mailboxId) throws RemoteException { } + @Override public void updateFolderList(long accountId) throws RemoteException { } + @Override public Bundle validate(HostAuth hostAuth) throws RemoteException { return null; } + @Override public void deleteAccountPIMData(long accountId) throws RemoteException { } + @Override public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { return 0; } + @Override public IBinder asBinder() { return null; } diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java new file mode 100644 index 000000000..892c40b39 --- /dev/null +++ b/src/com/android/email/service/ImapService.java @@ -0,0 +1,1717 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.email.service; + +import android.app.Service; +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.text.TextUtils; +import android.util.Log; + +import com.android.email.Email; +import com.android.email.LegacyConversions; +import com.android.email.NotificationController; +import com.android.email.mail.Store; +import com.android.emailcommon.AccountManagerTypes; +import com.android.emailcommon.Api; +import com.android.emailcommon.Logging; +import com.android.emailcommon.TrafficFlags; +import com.android.emailcommon.internet.MimeUtility; +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.FolderType; +import com.android.emailcommon.mail.Folder.MessageRetrievalListener; +import com.android.emailcommon.mail.Folder.MessageUpdateCallbacks; +import com.android.emailcommon.mail.Folder.OpenMode; +import com.android.emailcommon.mail.Message; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.mail.Part; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.EmailContent; +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.HostAuth; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.EmailServiceStatus; +import com.android.emailcommon.service.IEmailService; +import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.service.SearchParams; +import com.android.emailcommon.utility.AttachmentUtilities; +import com.android.emailcommon.utility.ConversionUtilities; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; + +public class ImapService extends Service { + private static final String TAG = "ImapService"; + private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); + + private static final Flag[] FLAG_LIST_SEEN = new Flag[] { Flag.SEEN }; + private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; + private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; + + /** + * Cache search results by account; this allows for "load more" support without having to + * redo the search (which can be quite slow). SortableMessage is a smallish class, so memory + * shouldn't be an issue + */ + private static final HashMap sSearchResults = + new HashMap(); + + /** + * We write this into the serverId field of messages that will never be upsynced. + */ + private static final String LOCAL_SERVERID_PREFIX = "Local-"; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + return Service.START_STICKY; + } + + // Callbacks as set up via setCallback + private static final RemoteCallbackList mCallbackList = + new RemoteCallbackList(); + + private interface ServiceCallbackWrapper { + public void call(IEmailServiceCallback cb) throws RemoteException; + } + + /** + * Proxy that can be used by various sync adapters to tie into ExchangeService's callback system + * Used this way: ExchangeService.callback().callbackMethod(args...); + * The proxy wraps checking for existence of a ExchangeService instance + * Failures of these callbacks can be safely ignored. + */ + static private final IEmailServiceCallback.Stub sCallbackProxy = + new IEmailServiceCallback.Stub() { + + /** + * Broadcast a callback to the everyone that's registered + * + * @param wrapper the ServiceCallbackWrapper used in the broadcast + */ + private synchronized void broadcastCallback(ServiceCallbackWrapper wrapper) { + RemoteCallbackList callbackList = mCallbackList; + if (callbackList != null) { + // Call everyone on our callback list + int count = callbackList.beginBroadcast(); + try { + for (int i = 0; i < count; i++) { + try { + wrapper.call(callbackList.getBroadcastItem(i)); + } catch (RemoteException e) { + // Safe to ignore + } catch (RuntimeException e) { + // We don't want an exception in one call to prevent other calls, so + // we'll just log this and continue + Log.e(TAG, "Caught RuntimeException in broadcast", e); + } + } + } finally { + // No matter what, we need to finish the broadcast + callbackList.finishBroadcast(); + } + } + } + + @Override + public void loadAttachmentStatus(final long messageId, final long attachmentId, + final int status, final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.loadAttachmentStatus(messageId, attachmentId, status, progress); + } + }); + } + + @Override + public void loadMessageStatus(final long messageId, final int status, final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.loadMessageStatus(messageId, status, progress); + } + }); + } + + @Override + public void sendMessageStatus(final long messageId, final String subject, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.sendMessageStatus(messageId, subject, status, progress); + } + }); + } + + @Override + public void syncMailboxListStatus(final long accountId, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.syncMailboxListStatus(accountId, status, progress); + } + }); + } + + @Override + public void syncMailboxStatus(final long mailboxId, final int status, + final int progress) { + broadcastCallback(new ServiceCallbackWrapper() { + @Override + public void call(IEmailServiceCallback cb) throws RemoteException { + cb.syncMailboxStatus(mailboxId, status, progress); + } + }); + } + }; + + /** + * Create our EmailService implementation here. + */ + private final IEmailService.Stub mBinder = new IEmailService.Stub() { + + @Override + public int getApiLevel() { + return Api.LEVEL; + } + + @Override + public Bundle validate(HostAuth hostAuth) throws RemoteException { + return null; + } + + @Override + public Bundle autoDiscover(String userName, String password) throws RemoteException { + return null; + } + + @Override + public void startSync(long mailboxId, boolean userRequest) throws RemoteException { + Context context = getApplicationContext(); + Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); + if (mailbox == null) return; + Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); + if (account == null) return; + android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, + AccountManagerTypes.TYPE_POP_IMAP); + Log.d(TAG, "startSync API requesting sync"); + Bundle extras = new Bundle(); + extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true); + ContentResolver.requestSync(acct, EmailContent.AUTHORITY, extras); + } + + @Override + public void stopSync(long mailboxId) throws RemoteException { + } + + @Override + public void loadAttachment(long attachmentId, boolean background) throws RemoteException { + } + + @Override + public void updateFolderList(long accountId) throws RemoteException { + } + + @Override + public void hostChanged(long accountId) throws RemoteException { + } + + @Override + public void setLogging(int flags) throws RemoteException { + } + + @Override + public void sendMeetingResponse(long messageId, int response) throws RemoteException { + } + + @Override + public void loadMore(long messageId) throws RemoteException { + // Load a message for view... + Context context = getApplicationContext(); + try { + // 1. Resample the message, in case it disappeared or synced while + // this command was in queue + EmailContent.Message message = + EmailContent.Message.restoreMessageWithId(context, messageId); + if (message == null) { + sCallbackProxy.loadMessageStatus(messageId, + EmailServiceStatus.MESSAGE_NOT_FOUND, 0); + return; + } + if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { + // We should NEVER get here + sCallbackProxy.loadMessageStatus(messageId, 0, 100); + return; + } + + // 2. Open the remote folder. + // TODO combine with common code in loadAttachment + Account account = Account.restoreAccountWithId(context, message.mAccountKey); + Mailbox mailbox = Mailbox.restoreMailboxWithId(context, message.mMailboxKey); + if (account == null || mailbox == null) { + //mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); + return; + } + TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); + + Store remoteStore = Store.getInstance(account, context); + 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); + + // 3. Set up to download the entire message + Message remoteMessage = remoteFolder.getMessage(message.mServerId); + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.BODY); + remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); + + // 4. Write to provider + copyOneMessageToProvider(context, remoteMessage, account, mailbox, + EmailContent.Message.FLAG_LOADED_COMPLETE); + + // 5. Notify UI + sCallbackProxy.loadMessageStatus(messageId, 0, 100); + + } catch (MessagingException me) { + if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); + sCallbackProxy.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); + } catch (RuntimeException rte) { + sCallbackProxy.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); + } + } + + // The following three methods are not implemented in this version + @Override + public boolean createFolder(long accountId, String name) throws RemoteException { + return false; + } + + @Override + public boolean deleteFolder(long accountId, String name) throws RemoteException { + return false; + } + + @Override + public boolean renameFolder(long accountId, String oldName, String newName) + throws RemoteException { + return false; + } + + @Override + public void setCallback(IEmailServiceCallback cb) throws RemoteException { + mCallbackList.register(cb); + } + + /** + * Delete PIM (calendar, contacts) data for the specified account + * + * @param accountId the account whose data should be deleted + * @throws RemoteException + */ + @Override + public void deleteAccountPIMData(long accountId) throws RemoteException { + } + + @Override + public int searchMessages(long accountId, SearchParams searchParams, long destMailboxId) { + return 0; + } + + }; + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + /** + * Start foreground synchronization of the specified folder. This is called by + * synchronizeMailbox or checkMail. + * TODO this should use ID's instead of fully-restored objects + * @param account + * @param folder + * @throws MessagingException + */ + public static void synchronizeMailboxSynchronous(Context context, final Account account, + final Mailbox folder) throws MessagingException { + TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); + if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { + } + NotificationController nc = NotificationController.getInstance(context); + try { + processPendingActionsSynchronous(context, account); + + // Select generic sync or store-specific sync + SyncResults results = synchronizeMailboxGeneric(context, account, folder); + // The account might have been deleted + if (results == null) return; + // Clear authentication notification for this account + nc.cancelLoginFailedNotification(account.mId); + } catch (MessagingException e) { + if (Logging.LOGD) { + Log.v(Logging.LOG_TAG, "synchronizeMailbox", e); + } + if (e instanceof AuthenticationFailedException) { + // Generate authentication notification + nc.showLoginFailedNotification(account.mId); + } + throw e; + } + } + + /** + * Lightweight record for the first pass of message sync, where I'm just seeing if + * the local message requires sync. Later (for messages that need syncing) we'll do a full + * readout from the DB. + */ + private static class LocalMessageInfo { + private static final int COLUMN_ID = 0; + private static final int COLUMN_FLAG_READ = 1; + private static final int COLUMN_FLAG_FAVORITE = 2; + private static final int COLUMN_FLAG_LOADED = 3; + private static final int COLUMN_SERVER_ID = 4; + private static final int COLUMN_FLAGS = 7; + private static final String[] PROJECTION = new String[] { + EmailContent.RECORD_ID, + MessageColumns.FLAG_READ, MessageColumns.FLAG_FAVORITE, MessageColumns.FLAG_LOADED, + SyncColumns.SERVER_ID, MessageColumns.MAILBOX_KEY, MessageColumns.ACCOUNT_KEY, + MessageColumns.FLAGS + }; + + final long mId; + final boolean mFlagRead; + final boolean mFlagFavorite; + final int mFlagLoaded; + final String mServerId; + final int mFlags; + + public LocalMessageInfo(Cursor c) { + mId = c.getLong(COLUMN_ID); + mFlagRead = c.getInt(COLUMN_FLAG_READ) != 0; + mFlagFavorite = c.getInt(COLUMN_FLAG_FAVORITE) != 0; + mFlagLoaded = c.getInt(COLUMN_FLAG_LOADED); + mServerId = c.getString(COLUMN_SERVER_ID); + mFlags = c.getInt(COLUMN_FLAGS); + // Note: mailbox key and account key not needed - they are projected for the SELECT + } + } + + private static void saveOrUpdate(EmailContent content, Context context) { + if (content.isSaved()) { + content.update(context, content.toContentValues()); + } else { + content.save(context); + } + } + + /** + * 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 + */ + static void loadUnsyncedMessages(final Context context, 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() { + @Override + public void messageRetrieved(Message message) { + // Store the updated message locally and mark it fully loaded + copyOneMessageToProvider(context, 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(context, 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(context, message, account, toMailbox, + EmailContent.Message.FLAG_LOADED_COMPLETE); + } + } + + } + + public static void downloadFlagAndEnvelope(final Context context, 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( + context, 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, context); + // 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) { + } + }); + + } + + /** + * Synchronizer for IMAP. + * + * TODO Break this method up into smaller chunks. + * + * @param account the account to sync + * @param mailbox the mailbox to sync + * @return results of the sync pass + * @throws MessagingException + */ + private static SyncResults synchronizeMailboxGeneric(final Context context, + final Account account, final Mailbox mailbox) throws MessagingException { + + /* + * A list of IDs for messages that were downloaded and did not have the seen flag set. + * This serves as the "true" new message count reported to the user via notification. + */ + final ArrayList unseenMessages = new ArrayList(); + + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "*** synchronizeMailboxGeneric ***"); + } + ContentResolver resolver = context.getContentResolver(); + + // 0. We do not ever sync DRAFTS or OUTBOX (down or up) + if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { + int totalMessages = EmailContent.count(context, mailbox.getUri(), null, null); + return new SyncResults(totalMessages, unseenMessages); + } + + // 1. Get the message list from the local store and create an index of the uids + + Cursor localUidCursor = null; + HashMap localMessageMap = new HashMap(); + + try { + localUidCursor = resolver.query( + EmailContent.Message.CONTENT_URI, + LocalMessageInfo.PROJECTION, + EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + + " AND " + MessageColumns.MAILBOX_KEY + "=?", + new String[] { + String.valueOf(account.mId), + String.valueOf(mailbox.mId) + }, + null); + while (localUidCursor.moveToNext()) { + LocalMessageInfo info = new LocalMessageInfo(localUidCursor); + localMessageMap.put(info.mServerId, info); + } + } finally { + if (localUidCursor != null) { + localUidCursor.close(); + } + } + + // 2. Open the remote folder and create the remote folder if necessary + + Store remoteStore = Store.getInstance(account, context); + // The account might have been deleted + if (remoteStore == null) return null; + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); + + /* + * If the folder is a "special" folder we need to see if it exists + * on the remote server. It if does not exist we'll try to create it. If we + * can't create we'll abort. This will happen on every single Pop3 folder as + * designed and on Imap folders during error conditions. This allows us + * to treat Pop3 and Imap the same in this code. + */ + 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); + } + } + } + + // 3, Open the remote folder. This pre-loads certain metadata like message count. + remoteFolder.open(OpenMode.READ_WRITE); + + // 4. Trash any remote messages that are marked as trashed locally. + // TODO - this comment was here, but no code was here. + + // 5. Get the remote message count. + int remoteMessageCount = remoteFolder.getMessageCount(); + + // 6. Determine the limit # of messages to download + int visibleLimit = mailbox.mVisibleLimit; + if (visibleLimit <= 0) { + visibleLimit = Email.VISIBLE_LIMIT_DEFAULT; + } + + // 7. Create a list of messages to download + Message[] remoteMessages = new Message[0]; + final ArrayList unsyncedMessages = new ArrayList(); + HashMap remoteUidMap = new HashMap(); + + if (remoteMessageCount > 0) { + /* + * Message numbers start at 1. + */ + int remoteStart = Math.max(0, remoteMessageCount - visibleLimit) + 1; + int remoteEnd = remoteMessageCount; + remoteMessages = remoteFolder.getMessages(remoteStart, remoteEnd, null); + // TODO Why are we running through the list twice? Combine w/ for loop below + for (Message message : remoteMessages) { + remoteUidMap.put(message.getUid(), message); + } + + /* + * Get a list of the messages that are in the remote list but not on the + * local store, or messages that are in the local store but failed to download + * on the last sync. These are the new messages that we will download. + * Note, we also skip syncing messages which are flagged as "deleted message" sentinels, + * because they are locally deleted and we don't need or want the old message from + * the server. + */ + for (Message message : remoteMessages) { + LocalMessageInfo localMessage = localMessageMap.get(message.getUid()); + // localMessage == null -> message has never been created (not even headers) + // mFlagLoaded = UNLOADED -> message created, but none of body loaded + // mFlagLoaded = PARTIAL -> message created, a "sane" amt of body has been loaded + // mFlagLoaded = COMPLETE -> message body has been completely loaded + // mFlagLoaded = DELETED -> message has been deleted + // Only the first two of these are "unsynced", so let's retrieve them + if (localMessage == null || + (localMessage.mFlagLoaded == EmailContent.Message.FLAG_LOADED_UNLOADED)) { + unsyncedMessages.add(message); + } + } + } + + // 8. Download basic info about the new/unloaded messages (if any) + /* + * Fetch the flags and envelope only of the new messages. This is intended to get us + * critical data as fast as possible, and then we'll fill in the details. + */ + if (unsyncedMessages.size() > 0) { + downloadFlagAndEnvelope(context, account, mailbox, remoteFolder, unsyncedMessages, + localMessageMap, unseenMessages); + } + + // 9. Refresh the flags for any messages in the local store that we didn't just download. + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.FLAGS); + remoteFolder.fetch(remoteMessages, fp, null); + boolean remoteSupportsSeen = false; + boolean remoteSupportsFlagged = false; + boolean remoteSupportsAnswered = false; + for (Flag flag : remoteFolder.getPermanentFlags()) { + if (flag == Flag.SEEN) { + remoteSupportsSeen = true; + } + if (flag == Flag.FLAGGED) { + remoteSupportsFlagged = true; + } + if (flag == Flag.ANSWERED) { + remoteSupportsAnswered = true; + } + } + // Update SEEN/FLAGGED/ANSWERED (star) flags (if supported remotely - e.g. not for POP3) + if (remoteSupportsSeen || remoteSupportsFlagged || remoteSupportsAnswered) { + for (Message remoteMessage : remoteMessages) { + LocalMessageInfo localMessageInfo = localMessageMap.get(remoteMessage.getUid()); + if (localMessageInfo == null) { + continue; + } + boolean localSeen = localMessageInfo.mFlagRead; + boolean remoteSeen = remoteMessage.isSet(Flag.SEEN); + boolean newSeen = (remoteSupportsSeen && (remoteSeen != localSeen)); + boolean localFlagged = localMessageInfo.mFlagFavorite; + boolean remoteFlagged = remoteMessage.isSet(Flag.FLAGGED); + boolean newFlagged = (remoteSupportsFlagged && (localFlagged != remoteFlagged)); + int localFlags = localMessageInfo.mFlags; + boolean localAnswered = (localFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0; + boolean remoteAnswered = remoteMessage.isSet(Flag.ANSWERED); + boolean newAnswered = (remoteSupportsAnswered && (localAnswered != remoteAnswered)); + if (newSeen || newFlagged || newAnswered) { + Uri uri = ContentUris.withAppendedId( + EmailContent.Message.CONTENT_URI, localMessageInfo.mId); + ContentValues updateValues = new ContentValues(); + updateValues.put(MessageColumns.FLAG_READ, remoteSeen); + updateValues.put(MessageColumns.FLAG_FAVORITE, remoteFlagged); + if (remoteAnswered) { + localFlags |= EmailContent.Message.FLAG_REPLIED_TO; + } else { + localFlags &= ~EmailContent.Message.FLAG_REPLIED_TO; + } + updateValues.put(MessageColumns.FLAGS, localFlags); + resolver.update(uri, updateValues, null, null); + } + } + } + + // 10. Remove any messages that are in the local store but no longer on the remote store. + HashSet localUidsToDelete = new HashSet(localMessageMap.keySet()); + localUidsToDelete.removeAll(remoteUidMap.keySet()); + for (String uidToDelete : localUidsToDelete) { + LocalMessageInfo infoToDelete = localMessageMap.get(uidToDelete); + + // Delete associated data (attachment files) + // Attachment & Body records are auto-deleted when we delete the Message record + AttachmentUtilities.deleteAllAttachmentFiles(context, account.mId, + infoToDelete.mId); + + // Delete the message itself + Uri uriToDelete = ContentUris.withAppendedId( + EmailContent.Message.CONTENT_URI, infoToDelete.mId); + resolver.delete(uriToDelete, null, null); + + // Delete extra rows (e.g. synced or deleted) + Uri syncRowToDelete = ContentUris.withAppendedId( + EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); + resolver.delete(syncRowToDelete, null, null); + Uri deletERowToDelete = ContentUris.withAppendedId( + EmailContent.Message.UPDATED_CONTENT_URI, infoToDelete.mId); + resolver.delete(deletERowToDelete, null, null); + } + + loadUnsyncedMessages(context, account, remoteFolder, unsyncedMessages, mailbox); + + // 14. Clean up and report results + remoteFolder.close(false); + + return new SyncResults(remoteMessageCount, unseenMessages); + } + + /** + * Copy one downloaded message (which may have partially-loaded sections) + * into a newly created EmailProvider Message, given the account and mailbox + * + * @param message the remote message we've just downloaded + * @param account the account it will be stored into + * @param folder the mailbox it will be stored into + * @param loadStatus when complete, the message will be marked with this status (e.g. + * EmailContent.Message.LOADED) + */ + public static void copyOneMessageToProvider(Context context, Message message, Account account, + Mailbox folder, int loadStatus) { + EmailContent.Message localMessage = null; + Cursor c = null; + try { + c = context.getContentResolver().query( + EmailContent.Message.CONTENT_URI, + EmailContent.Message.CONTENT_PROJECTION, + EmailContent.MessageColumns.ACCOUNT_KEY + "=?" + + " AND " + MessageColumns.MAILBOX_KEY + "=?" + + " AND " + SyncColumns.SERVER_ID + "=?", + new String[] { + String.valueOf(account.mId), + String.valueOf(folder.mId), + String.valueOf(message.getUid()) + }, + null); + if (c.moveToNext()) { + localMessage = EmailContent.getContent(c, EmailContent.Message.class); + localMessage.mMailboxKey = folder.mId; + localMessage.mAccountKey = account.mId; + copyOneMessageToProvider(context, message, localMessage, loadStatus); + } + } finally { + if (c != null) { + c.close(); + } + } + } + + /** + * Copy one downloaded message (which may have partially-loaded sections) + * into an already-created EmailProvider Message + * + * @param message the remote message we've just downloaded + * @param localMessage the EmailProvider Message, already created + * @param loadStatus when complete, the message will be marked with this status (e.g. + * EmailContent.Message.LOADED) + * @param context the context to be used for EmailProvider + */ + public static void copyOneMessageToProvider(Context context, Message message, + EmailContent.Message localMessage, int loadStatus) { + try { + + EmailContent.Body body = EmailContent.Body.restoreBodyWithMessageId(context, + localMessage.mId); + if (body == null) { + body = new EmailContent.Body(); + } + try { + // Copy the fields that are available into the message object + LegacyConversions.updateMessageFields(localMessage, message, + localMessage.mAccountKey, localMessage.mMailboxKey); + + // Now process body parts & attachments + ArrayList viewables = new ArrayList(); + ArrayList attachments = new ArrayList(); + MimeUtility.collectParts(message, viewables, attachments); + + ConversionUtilities.updateBodyFields(body, localMessage, viewables); + + // Commit the message & body to the local store immediately + saveOrUpdate(localMessage, context); + saveOrUpdate(body, context); + + // process (and save) attachments + LegacyConversions.updateAttachments(context, localMessage, attachments); + + // One last update of message with two updated flags + localMessage.mFlagLoaded = loadStatus; + + ContentValues cv = new ContentValues(); + cv.put(EmailContent.MessageColumns.FLAG_ATTACHMENT, localMessage.mFlagAttachment); + cv.put(EmailContent.MessageColumns.FLAG_LOADED, localMessage.mFlagLoaded); + Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, + localMessage.mId); + context.getContentResolver().update(uri, cv, null, null); + + } catch (MessagingException me) { + Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me); + } + + } catch (RuntimeException rte) { + Log.e(Logging.LOG_TAG, "Error while storing downloaded message." + rte.toString()); + } catch (IOException ioe) { + Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); + } + } + + /** + * Find messages in the updated table that need to be written back to server. + * + * Handles: + * Read/Unread + * Flagged + * Append (upload) + * Move To Trash + * Empty trash + * TODO: + * Move + * + * @param account the account to scan for pending actions + * @throws MessagingException + */ + private static void processPendingActionsSynchronous(Context context, Account account) + throws MessagingException { + TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(context, account)); + String[] accountIdArgs = new String[] { Long.toString(account.mId) }; + + // Handle deletes first, it's always better to get rid of things first + processPendingDeletesSynchronous(context, account, accountIdArgs); + + // Handle uploads (currently, only to sent messages) + processPendingUploadsSynchronous(context, account, accountIdArgs); + + // Now handle updates / upsyncs + processPendingUpdatesSynchronous(context, account, 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 static Mailbox getRemoteMailboxForMessage(Context context, + 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 = context.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(context, message.mMailboxKey); + } + } + + /** + * Scan for messages that are in the Message_Deletes table, look for differences that + * we can deal with, and do the work. + * + * @param account + * @param resolver + * @param accountIdArgs + */ + private static void processPendingDeletesSynchronous(Context context, Account account, + String[] accountIdArgs) { + Cursor deletes = context.getContentResolver().query( + EmailContent.Message.DELETED_CONTENT_URI, + EmailContent.Message.CONTENT_PROJECTION, + EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, + EmailContent.MessageColumns.MAILBOX_KEY); + long lastMessageId = -1; + try { + // Defer setting up the store until we know we need to access it + Store remoteStore = null; + // loop through messages marked as deleted + while (deletes.moveToNext()) { + boolean deleteFromTrash = false; + + EmailContent.Message oldMessage = + EmailContent.getContent(deletes, EmailContent.Message.class); + + if (oldMessage != null) { + lastMessageId = oldMessage.mId; + + Mailbox mailbox = getRemoteMailboxForMessage(context, 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, context); + } + + // Dispatch here for specific change types + if (deleteFromTrash) { + // Move message to trash + processPendingDeleteFromTrash(context, remoteStore, account, mailbox, + oldMessage); + } + } + + // Finally, delete the update + Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, + oldMessage.mId); + context.getContentResolver().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. + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "Unable to process pending delete for id=" + + lastMessageId + ": " + me); + } + } finally { + deletes.close(); + } + } + + /** + * Scan for messages that are in Sent, and are in need of upload, + * and send them to the server. "In need of upload" is defined as: + * serverId == null (no UID has been assigned) + * or + * message is in the updated list + * + * Note we also look for messages that are moving from drafts->outbox->sent. They never + * go through "drafts" or "outbox" on the server, so we hang onto these until they can be + * uploaded directly to the Sent folder. + * + * @param account + * @param resolver + * @param accountIdArgs + */ + private static void processPendingUploadsSynchronous(Context context, Account account, + String[] accountIdArgs) { + ContentResolver resolver = context.getContentResolver(); + // Find the Sent folder (since that's all we're uploading for now + Cursor mailboxes = resolver.query(Mailbox.CONTENT_URI, Mailbox.ID_PROJECTION, + MailboxColumns.ACCOUNT_KEY + "=?" + + " and " + MailboxColumns.TYPE + "=" + Mailbox.TYPE_SENT, + accountIdArgs, null); + long lastMessageId = -1; + try { + // Defer setting up the store until we know we need to access it + Store remoteStore = null; + while (mailboxes.moveToNext()) { + long mailboxId = mailboxes.getLong(Mailbox.ID_PROJECTION_COLUMN); + String[] mailboxKeyArgs = new String[] { Long.toString(mailboxId) }; + // Demand load mailbox + Mailbox mailbox = null; + + // First handle the "new" messages (serverId == null) + Cursor upsyncs1 = resolver.query(EmailContent.Message.CONTENT_URI, + EmailContent.Message.ID_PROJECTION, + EmailContent.Message.MAILBOX_KEY + "=?" + + " and (" + EmailContent.Message.SERVER_ID + " is null" + + " or " + EmailContent.Message.SERVER_ID + "=''" + ")", + mailboxKeyArgs, + null); + try { + while (upsyncs1.moveToNext()) { + // Load the remote store if it will be needed + if (remoteStore == null) { + remoteStore = Store.getInstance(account, context); + } + // Load the mailbox if it will be needed + if (mailbox == null) { + mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); + if (mailbox == null) { + continue; // Mailbox removed. Move to the next message. + } + } + // upsync the message + long id = upsyncs1.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); + lastMessageId = id; + processUploadMessage(context, remoteStore, account, mailbox, id); + } + } finally { + if (upsyncs1 != null) { + upsyncs1.close(); + } + } + + // Next, handle any updates (e.g. edited in place, although this shouldn't happen) + Cursor upsyncs2 = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, + EmailContent.Message.ID_PROJECTION, + EmailContent.MessageColumns.MAILBOX_KEY + "=?", mailboxKeyArgs, + null); + try { + while (upsyncs2.moveToNext()) { + // Load the remote store if it will be needed + if (remoteStore == null) { + remoteStore = Store.getInstance(account, context); + } + // Load the mailbox if it will be needed + if (mailbox == null) { + mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); + if (mailbox == null) { + continue; // Mailbox removed. Move to the next message. + } + } + // upsync the message + long id = upsyncs2.getLong(EmailContent.Message.ID_PROJECTION_COLUMN); + lastMessageId = id; + processUploadMessage(context, remoteStore, account, mailbox, id); + } + } finally { + if (upsyncs2 != null) { + upsyncs2.close(); + } + } + } + } 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. + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "Unable to process pending upsync for id=" + + lastMessageId + ": " + me); + } + } finally { + if (mailboxes != null) { + mailboxes.close(); + } + } + } + + /** + * Scan for messages that are in the Message_Updates table, look for differences that + * we can deal with, and do the work. + * + * @param account + * @param resolver + * @param accountIdArgs + */ + private static void processPendingUpdatesSynchronous(Context context, Account account, + String[] accountIdArgs) { + ContentResolver resolver = context.getContentResolver(); + Cursor updates = resolver.query(EmailContent.Message.UPDATED_CONTENT_URI, + EmailContent.Message.CONTENT_PROJECTION, + EmailContent.MessageColumns.ACCOUNT_KEY + "=?", accountIdArgs, + EmailContent.MessageColumns.MAILBOX_KEY); + long lastMessageId = -1; + 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 needing updates + while (updates.moveToNext()) { + boolean changeMoveToTrash = false; + boolean changeRead = false; + boolean changeFlagged = false; + boolean changeMailbox = false; + boolean changeAnswered = false; + + EmailContent.Message oldMessage = + EmailContent.getContent(updates, EmailContent.Message.class); + lastMessageId = oldMessage.mId; + EmailContent.Message newMessage = + EmailContent.Message.restoreMessageWithId(context, oldMessage.mId); + if (newMessage != null) { + mailbox = Mailbox.restoreMailboxWithId(context, newMessage.mMailboxKey); + if (mailbox == null) { + continue; // Mailbox removed. Move to the next message. + } + if (oldMessage.mMailboxKey != newMessage.mMailboxKey) { + if (mailbox.mType == Mailbox.TYPE_TRASH) { + changeMoveToTrash = true; + } else { + changeMailbox = true; + } + } + changeRead = oldMessage.mFlagRead != newMessage.mFlagRead; + changeFlagged = oldMessage.mFlagFavorite != newMessage.mFlagFavorite; + changeAnswered = (oldMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != + (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO); + } + + // Load the remote store if it will be needed + if (remoteStore == null && + (changeMoveToTrash || changeRead || changeFlagged || changeMailbox || + changeAnswered)) { + remoteStore = Store.getInstance(account, context); + } + + // Dispatch here for specific change types + if (changeMoveToTrash) { + // Move message to trash + processPendingMoveToTrash(context, remoteStore, account, mailbox, oldMessage, + newMessage); + } else if (changeRead || changeFlagged || changeMailbox || changeAnswered) { + processPendingDataChange(context, remoteStore, mailbox, changeRead, + changeFlagged, changeMailbox, changeAnswered, oldMessage, newMessage); + } + + // Finally, delete the update + Uri uri = ContentUris.withAppendedId(EmailContent.Message.UPDATED_CONTENT_URI, + 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. + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "Unable to process pending update for id=" + + lastMessageId + ": " + me); + } + } finally { + updates.close(); + } + } + + /** + * Upsync an entire message. This must also unwind whatever triggered it (either by + * updating the serverId, or by deleting the update record, or it's going to keep happening + * over and over again. + * + * Note: If the message is being uploaded into an unexpected mailbox, we *do not* upload. + * This is to avoid unnecessary uploads into the trash. Although the caller attempts to select + * only the Drafts and Sent folders, this can happen when the update record and the current + * record mismatch. In this case, we let the update record remain, because the filters + * in processPendingUpdatesSynchronous() will pick it up as a move and handle it (or drop it) + * appropriately. + * + * @param resolver + * @param remoteStore + * @param account + * @param mailbox the actual mailbox + * @param messageId + */ + private static void processUploadMessage(Context context, Store remoteStore, + Account account, Mailbox mailbox, long messageId) + throws MessagingException { + EmailContent.Message newMessage = + EmailContent.Message.restoreMessageWithId(context, messageId); + boolean deleteUpdate = false; + if (newMessage == null) { + deleteUpdate = true; + Log.d(Logging.LOG_TAG, "Upsync failed for null message, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_DRAFTS) { + deleteUpdate = false; + Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=drafts, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_OUTBOX) { + deleteUpdate = false; + Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=outbox, id=" + messageId); + } else if (mailbox.mType == Mailbox.TYPE_TRASH) { + deleteUpdate = false; + Log.d(Logging.LOG_TAG, "Upsync skipped for mailbox=trash, id=" + messageId); + } else if (newMessage != null && newMessage.mMailboxKey != mailbox.mId) { + deleteUpdate = false; + Log.d(Logging.LOG_TAG, "Upsync skipped; mailbox changed, id=" + messageId); + } else { +// Log.d(Logging.LOG_TAG, "Upsyc triggered for message id=" + messageId); +// deleteUpdate = processPendingAppend(context, remoteStore, account, mailbox, + //newMessage); + } + if (deleteUpdate) { + // Finally, delete the update (if any) + Uri uri = ContentUris.withAppendedId( + EmailContent.Message.UPDATED_CONTENT_URI, messageId); + context.getContentResolver().delete(uri, null, null); + } + } + + /** + * Upsync changes to read, flagged, or mailbox + * + * @param remoteStore the remote store for this mailbox + * @param mailbox the mailbox the message is stored in + * @param changeRead whether the message's read state has changed + * @param changeFlagged whether the message's flagged state has changed + * @param changeMailbox whether the message's mailbox has changed + * @param oldMessage the message in it's pre-change state + * @param newMessage the current version of the message + */ + private static void processPendingDataChange(final Context context, Store remoteStore, + Mailbox mailbox, boolean changeRead, boolean changeFlagged, boolean changeMailbox, + boolean changeAnswered, EmailContent.Message oldMessage, + final EmailContent.Message newMessage) throws MessagingException { + // 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(context, oldMessage); + + // 0. No remote update if the message is local-only + if (newMessage.mServerId == null || newMessage.mServerId.equals("") + || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX) || (mailbox == null)) { + return; + } + + // 1. No remote update for DRAFTS or OUTBOX + if (mailbox.mType == Mailbox.TYPE_DRAFTS || mailbox.mType == Mailbox.TYPE_OUTBOX) { + return; + } + + // 2. Open the remote store & folder + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); + if (!remoteFolder.exists()) { + return; + } + remoteFolder.open(OpenMode.READ_WRITE); + if (remoteFolder.getMode() != OpenMode.READ_WRITE) { + return; + } + + // 3. Finally, apply the changes to the message + Message remoteMessage = remoteFolder.getMessage(newMessage.mServerId); + if (remoteMessage == null) { + return; + } + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, + "Update for msg id=" + newMessage.mId + + " read=" + newMessage.mFlagRead + + " flagged=" + newMessage.mFlagFavorite + + " answered=" + + ((newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0) + + " new mailbox=" + newMessage.mMailboxKey); + } + Message[] messages = new Message[] { remoteMessage }; + if (changeRead) { + remoteFolder.setFlags(messages, FLAG_LIST_SEEN, newMessage.mFlagRead); + } + if (changeFlagged) { + remoteFolder.setFlags(messages, FLAG_LIST_FLAGGED, newMessage.mFlagFavorite); + } + if (changeAnswered) { + remoteFolder.setFlags(messages, FLAG_LIST_ANSWERED, + (newMessage.mFlags & EmailContent.Message.FLAG_REPLIED_TO) != 0); + } + if (changeMailbox) { + Folder toFolder = remoteStore.getFolder(newMailbox.mServerId); + if (!remoteFolder.exists()) { + return; + } + // We may need the message id to search for the message in the destination folder + remoteMessage.setMessageId(newMessage.mMessageId); + // Copy the message to its new folder + remoteFolder.copyMessages(messages, toFolder, new MessageUpdateCallbacks() { + @Override + public void onMessageUidChange(Message message, String newUid) { + ContentValues cv = new ContentValues(); + cv.put(EmailContent.Message.SERVER_ID, newUid); + // We only have one message, so, any updates _must_ be for it. Otherwise, + // we'd have to cycle through to find the one with the same server ID. + context.getContentResolver().update(ContentUris.withAppendedId( + EmailContent.Message.CONTENT_URI, newMessage.mId), cv, null, null); + } + @Override + public void onMessageNotFound(Message message) { + } + }); + // Delete the message from the remote source folder + remoteMessage.setFlag(Flag.DELETED, true); + remoteFolder.expunge(); + } + remoteFolder.close(false); + } + + /** + * Process a pending trash message command. + * + * @param remoteStore the remote store we're working in + * @param account The account in which we are working + * @param newMailbox The local trash mailbox + * @param oldMessage The message copy that was saved in the updates shadow table + * @param newMessage The message that was moved to the mailbox + */ + private static void processPendingMoveToTrash(final Context context, Store remoteStore, + Account account, Mailbox newMailbox, EmailContent.Message oldMessage, + final EmailContent.Message newMessage) throws MessagingException { + + // 0. No remote move if the message is local-only + if (newMessage.mServerId == null || newMessage.mServerId.equals("") + || newMessage.mServerId.startsWith(LOCAL_SERVERID_PREFIX)) { + return; + } + + // 1. Escape early if we can't find the local mailbox + // TODO smaller projection here + Mailbox oldMailbox = getRemoteMailboxForMessage(context, oldMessage); + if (oldMailbox == null) { + // can't find old mailbox, it may have been deleted. just return. + return; + } + // 2. We don't support delete-from-trash here + if (oldMailbox.mType == Mailbox.TYPE_TRASH) { + return; + } + + // 3. If DELETE_POLICY_NEVER, simply write back the deleted sentinel and return + // + // This sentinel takes the place of the server-side message, and locally "deletes" it + // by inhibiting future sync or display of the message. It will eventually go out of + // scope when it becomes old, or is deleted on the server, and the regular sync code + // will clean it up for us. + if (account.getDeletePolicy() == Account.DELETE_POLICY_NEVER) { + EmailContent.Message sentinel = new EmailContent.Message(); + sentinel.mAccountKey = oldMessage.mAccountKey; + sentinel.mMailboxKey = oldMessage.mMailboxKey; + sentinel.mFlagLoaded = EmailContent.Message.FLAG_LOADED_DELETED; + sentinel.mFlagRead = true; + sentinel.mServerId = oldMessage.mServerId; + sentinel.save(context); + + return; + } + + // The rest of this method handles server-side deletion + + // 4. Find the remote mailbox (that we deleted from), and open it + Folder remoteFolder = remoteStore.getFolder(oldMailbox.mServerId); + if (!remoteFolder.exists()) { + return; + } + + remoteFolder.open(OpenMode.READ_WRITE); + if (remoteFolder.getMode() != OpenMode.READ_WRITE) { + remoteFolder.close(false); + return; + } + + // 5. Find the remote original message + Message remoteMessage = remoteFolder.getMessage(oldMessage.mServerId); + if (remoteMessage == null) { + remoteFolder.close(false); + return; + } + + // 6. Find the remote trash folder, and create it if not found + Folder remoteTrashFolder = remoteStore.getFolder(newMailbox.mServerId); + if (!remoteTrashFolder.exists()) { + /* + * If the remote trash folder doesn't exist we try to create it. + */ + remoteTrashFolder.create(FolderType.HOLDS_MESSAGES); + } + + // 7. Try to copy the message into the remote trash folder + // Note, this entire section will be skipped for POP3 because there's no remote trash + if (remoteTrashFolder.exists()) { + /* + * Because remoteTrashFolder may be new, we need to explicitly open it + */ + remoteTrashFolder.open(OpenMode.READ_WRITE); + if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { + remoteFolder.close(false); + remoteTrashFolder.close(false); + return; + } + + remoteFolder.copyMessages(new Message[] { remoteMessage }, remoteTrashFolder, + new Folder.MessageUpdateCallbacks() { + @Override + public void onMessageUidChange(Message message, String newUid) { + // update the UID in the local trash folder, because some stores will + // have to change it when copying to remoteTrashFolder + ContentValues cv = new ContentValues(); + cv.put(EmailContent.Message.SERVER_ID, newUid); + context.getContentResolver().update(newMessage.getUri(), cv, null, null); + } + + /** + * This will be called if the deleted message doesn't exist and can't be + * deleted (e.g. it was already deleted from the server.) In this case, + * attempt to delete the local copy as well. + */ + @Override + public void onMessageNotFound(Message message) { + context.getContentResolver().delete(newMessage.getUri(), null, null); + } + }); + remoteTrashFolder.close(false); + } + + // 8. Delete the message from the remote source folder + remoteMessage.setFlag(Flag.DELETED, true); + remoteFolder.expunge(); + remoteFolder.close(false); + } + + /** + * Process a pending trash message command. + * + * @param remoteStore the remote store we're working in + * @param account The account in which we are working + * @param oldMailbox The local trash mailbox + * @param oldMessage The message that was deleted from the trash + */ + private static void processPendingDeleteFromTrash(Context context, Store remoteStore, + Account account, Mailbox oldMailbox, EmailContent.Message oldMessage) + throws MessagingException { + + // 1. We only support delete-from-trash here + if (oldMailbox.mType != Mailbox.TYPE_TRASH) { + return; + } + + // 2. Find the remote trash folder (that we are deleting from), and open it + Folder remoteTrashFolder = remoteStore.getFolder(oldMailbox.mServerId); + if (!remoteTrashFolder.exists()) { + return; + } + + remoteTrashFolder.open(OpenMode.READ_WRITE); + if (remoteTrashFolder.getMode() != OpenMode.READ_WRITE) { + remoteTrashFolder.close(false); + return; + } + + // 3. Find the remote original message + Message remoteMessage = remoteTrashFolder.getMessage(oldMessage.mServerId); + if (remoteMessage == null) { + remoteTrashFolder.close(false); + return; + } + + // 4. Delete the message from the remote trash folder + remoteMessage.setFlag(Flag.DELETED, true); + remoteTrashFolder.expunge(); + remoteTrashFolder.close(false); + } + + /** Results of the latest synchronization. */ + private static class SyncResults { + /** The total # of messages in the folder */ + public final int mTotalMessages; + /** A list of new message IDs; must not be {@code null} */ + public final ArrayList mAddedMessages; + + public SyncResults(int totalMessages, ArrayList addedMessages) { + if (addedMessages == null) { + throw new IllegalArgumentException("addedMessages must not be null"); + } + mTotalMessages = totalMessages; + mAddedMessages = addedMessages; + } + } + + /** + * 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 int searchMailbox(Context context, long accountId, SearchParams searchParams, + long destMailboxId) throws MessagingException { + try { + return searchMailboxImpl(context, accountId, searchParams, destMailboxId); + } finally { + // Tell UI + } + } + + private int searchMailboxImpl(final Context context, long accountId, SearchParams searchParams, + final long destMailboxId) throws MessagingException { + final Account account = Account.restoreAccountWithId(context, accountId); + final Mailbox mailbox = Mailbox.restoreMailboxWithId(context, searchParams.mMailboxId); + final Mailbox destMailbox = Mailbox.restoreMailboxWithId(context, destMailboxId); + if (account == null || mailbox == null || destMailbox == null) { + Log.d(Logging.LOG_TAG, "Attempted search for " + searchParams + + " but account or mailbox information was missing"); + return 0; + } + + // Tell UI that we're loading messages + + Store remoteStore = Store.getInstance(account, context); + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); + remoteFolder.open(OpenMode.READ_WRITE); + + SortableMessage[] sortableMessages = new SortableMessage[0]; + if (searchParams.mOffset == 0) { + // Get the "bare" messages (basically uid) + Message[] remoteMessages = remoteFolder.getMessages(searchParams, null); + int remoteCount = remoteMessages.length; + if (remoteCount > 0) { + 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 of request; + // those that do will see messages arrive from newest to oldest + 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; + } + }); + sSearchResults.put(accountId, sortableMessages); + } + } else { + sortableMessages = sSearchResults.get(accountId); + } + + final int numSearchResults = sortableMessages.length; + final int numToLoad = + Math.min(numSearchResults - searchParams.mOffset, searchParams.mLimit); + if (numToLoad <= 0) { + return 0; + } + + final ArrayList messageList = new ArrayList(); + for (int i = searchParams.mOffset; i < numToLoad + searchParams.mOffset; 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() { + @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 + 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, context); + 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; + } + copyOneMessageToProvider(context, message, localMessage, flag); + } 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) { + } + }); + return numSearchResults; + } +} \ No newline at end of file diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 4b3f31a54..9065e67fc 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -69,8 +69,6 @@ public class MailService extends Service { "com.android.email.intent.action.MAIL_SERVICE_CANCEL"; private static final String ACTION_SEND_PENDING_MAIL = "com.android.email.intent.action.MAIL_SERVICE_SEND_PENDING"; - private static final String ACTION_DELETE_EXCHANGE_ACCOUNTS = - "com.android.email.intent.action.MAIL_SERVICE_DELETE_EXCHANGE_ACCOUNTS"; private static final String EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; @@ -114,13 +112,6 @@ public class MailService extends Service { context.startService(i); } - public static void actionDeleteExchangeAccounts(Context context) { - Intent i = new Intent(); - i.setClass(context, MailService.class); - i.setAction(MailService.ACTION_DELETE_EXCHANGE_ACCOUNTS); - context.startService(i); - } - /** * Entry point for AttachmentDownloadService to ask that pending mail be sent * @param context the caller's context @@ -157,7 +148,7 @@ public class MailService extends Service { final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - if (ACTION_CHECK_MAIL.equals(action)) { + if ((ACTION_CHECK_MAIL).equals(action)) { // DB access required to satisfy this intent, so offload from UI thread EmailAsyncTask.runAsyncParallel(new Runnable() { @Override @@ -180,7 +171,10 @@ public class MailService extends Service { synchronized(mSyncReports) { for (AccountSyncReport report: mSyncReports.values()) { if (report.accountId == accountId) { - if (report.syncEnabled) { + // Only sync POP3 here (will remove POP3 sync soon) + if (report.syncEnabled && + Account.getProtocol(MailService.this, accountId) + .equals(HostAuth.SCHEME_POP3)) { syncStarted = syncOneAccount(mController, accountId, startId); } @@ -211,31 +205,6 @@ public class MailService extends Service { cancel(); stopSelf(startId); } - else if (ACTION_DELETE_EXCHANGE_ACCOUNTS.equals(action)) { - if (Email.DEBUG) { - Log.d(LOG_TAG, "action: delete exchange accounts"); - } - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - Cursor c = mContentResolver.query(Account.CONTENT_URI, Account.ID_PROJECTION, - null, null, null); - try { - while (c.moveToNext()) { - long accountId = c.getLong(Account.ID_PROJECTION_COLUMN); - if ("eas".equals(Account.getProtocol(mContext, accountId))) { - // Always log this - Log.d(LOG_TAG, "Deleting EAS account: " + accountId); - mController.deleteAccountSync(accountId, mContext); - } - } - } finally { - c.close(); - } - } - }); - stopSelf(startId); - } else if (ACTION_SEND_PENDING_MAIL.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: send pending mail"); diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/PopImapSyncAdapterService.java index ac47bbab5..121383f97 100644 --- a/src/com/android/email/service/PopImapSyncAdapterService.java +++ b/src/com/android/email/service/PopImapSyncAdapterService.java @@ -16,7 +16,6 @@ package com.android.email.service; -import android.accounts.Account; import android.accounts.OperationCanceledException; import android.app.Service; import android.content.AbstractThreadedSyncAdapter; @@ -30,11 +29,15 @@ import android.os.Bundle; import android.os.IBinder; import android.util.Log; -import com.android.email.Controller; -import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent.AccountColumns; +import com.android.emailcommon.provider.EmailContent.Message; +import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.provider.Mailbox; +import java.util.ArrayList; + public class PopImapSyncAdapterService extends Service { private static final String TAG = "PopImapSyncAdapterService"; private static SyncAdapterImpl sSyncAdapter = null; @@ -53,7 +56,7 @@ public class PopImapSyncAdapterService extends Service { } @Override - public void onPerformSync(Account account, Bundle extras, + public void onPerformSync(android.accounts.Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) { try { PopImapSyncAdapterService.performSync(mContext, account, extras, @@ -78,29 +81,96 @@ public class PopImapSyncAdapterService extends Service { return sSyncAdapter.getSyncAdapterBinder(); } + private static void sync(Context context, long mailboxId, SyncResult syncResult) { + Mailbox mailbox = Mailbox.restoreMailboxWithId(context, mailboxId); + if (mailbox == null) return; + Log.d(TAG, "Mailbox: " + mailbox.mDisplayName); + Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); + if (account == null) return; + try { + ImapService.synchronizeMailboxSynchronous(context, account, mailbox); + } catch (MessagingException e) { + int cause = e.getExceptionType(); + switch(cause) { + case MessagingException.IOERROR: + syncResult.stats.numIoExceptions++; + break; + case MessagingException.AUTHENTICATION_FAILED: + syncResult.stats.numAuthExceptions++; + break; + } + } + } + /** * Partial integration with system SyncManager; we initiate manual syncs upon request */ - private static void performSync(Context context, Account account, Bundle extras, - String authority, ContentProviderClient provider, SyncResult syncResult) - throws OperationCanceledException { - if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false)) { - String emailAddress = account.name; - // Find an EmailProvider account with the Account's email address - Cursor c = context.getContentResolver().query( - com.android.emailcommon.provider.Account.CONTENT_URI, - EmailContent.ID_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", - new String[] {emailAddress}, null); - if (c.moveToNext()) { - // If we have one, find the inbox and start it syncing - long accountId = c.getLong(EmailContent.ID_PROJECTION_COLUMN); - long mailboxId = Mailbox.findMailboxOfType(context, accountId, - Mailbox.TYPE_INBOX); - if (mailboxId > 0) { - Log.d(TAG, "Starting manual sync for account " + emailAddress); - Controller.getInstance(context).updateMailbox(accountId, mailboxId, false); + private static void performSync(Context context, android.accounts.Account account, + Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) + throws OperationCanceledException { + // Find an EmailProvider account with the Account's email address + Cursor c = null; + try { + c = provider.query(com.android.emailcommon.provider.Account.CONTENT_URI, + Account.CONTENT_PROJECTION, AccountColumns.EMAIL_ADDRESS + "=?", + new String[] {account.name}, null); + if (c != null && c.moveToNext()) { + Account acct = new Account(); + acct.restore(c); + String protocol = acct.getProtocol(context); + if (protocol.equals(HostAuth.SCHEME_IMAP)) { + if (extras.getBoolean(ContentResolver.SYNC_EXTRAS_UPLOAD)) { + Log.d(TAG, "Upload sync request for " + acct.mDisplayName); + // See if any boxes have mail... + Cursor updatesCursor = provider.query(Message.UPDATED_CONTENT_URI, + new String[] {Message.MAILBOX_KEY}, + Message.ACCOUNT_KEY + "=?", + new String[] {Long.toString(acct.mId)}, + null); + if ((updatesCursor == null) || (updatesCursor.getCount() == 0)) return; + ArrayList mailboxesToUpdate = new ArrayList(); + while (updatesCursor.moveToNext()) { + Long mailboxId = updatesCursor.getLong(0); + if (!mailboxesToUpdate.contains(mailboxId)) { + mailboxesToUpdate.add(mailboxId); + } + } + for (long mailboxId: mailboxesToUpdate) { + sync(context, mailboxId, syncResult); + } + } else { + Log.d(TAG, "Sync request for " + acct.mDisplayName); + Log.d(TAG, extras.toString()); + long mailboxId = extras.getLong("MAILBOX_ID", Mailbox.NO_MAILBOX); + boolean isInbox = false; + if (mailboxId == Mailbox.NO_MAILBOX) { + mailboxId = Mailbox.findMailboxOfType(context, acct.mId, + Mailbox.TYPE_INBOX); + isInbox = true; + } + if (mailboxId == Mailbox.NO_MAILBOX) return; + sync(context, mailboxId, syncResult); + + // Convert from minutes to seconds + int syncFrequency = acct.mSyncInterval * 60; + // Values < 0 are for "never" or "push"; 0 is undefined + if (syncFrequency <= 0) return; + Bundle ex = new Bundle(); + if (!isInbox) { + ex.putLong("MAILBOX_ID", mailboxId); + } + Log.d(TAG, "Setting periodic sync for " + acct.mDisplayName + ": " + + syncFrequency + " seconds"); + ContentResolver.addPeriodicSync(account, authority, ex, syncFrequency); + } } } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (c != null) { + c.close(); + } } } } \ No newline at end of file