diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 8cdd3439c..a646ddb49 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -406,6 +406,17 @@ + + + + + + mListeners = new HashSet(); /*package*/ final ConcurrentHashMap mLegacyControllerMap = @@ -102,11 +92,6 @@ public class Controller { }; private static final int MESSAGEID_TO_ACCOUNTID_COLUMN_ACCOUNTID = 1; - private static final String[] BODY_SOURCE_KEY_PROJECTION = - new String[] {Body.SOURCE_MESSAGE_KEY}; - private static final int BODY_SOURCE_KEY_COLUMN = 0; - private static final String WHERE_MESSAGE_KEY = Body.MESSAGE_KEY + "=?"; - private static final String MAILBOXES_FOR_ACCOUNT_SELECTION = MailboxColumns.ACCOUNT_KEY + "=?"; private static final String MAILBOXES_FOR_ACCOUNT_EXCEPT_ACCOUNT_MAILBOX_SELECTION = MAILBOXES_FOR_ACCOUNT_SELECTION + " AND " + MailboxColumns.TYPE + "!=" + @@ -122,8 +107,6 @@ public class Controller { protected Controller(Context _context) { mContext = _context.getApplicationContext(); mProviderContext = _context; - mLegacyController = MessagingController.getInstance(mProviderContext, this); - mLegacyController.addListener(mLegacyListener); } /** @@ -135,16 +118,6 @@ public class Controller { mInUnitTests = inUnitTests; } - /** - * Cleanup for test. Mustn't be called for the regular {@link Controller}, as it's a - * singleton and lives till the process finishes. - * - *

However, this method MUST be called for mock instances. - */ - public void cleanupForTest() { - mLegacyController.removeListener(mLegacyListener); - } - /** * Gets or creates the singleton instance of Controller. */ @@ -309,8 +282,8 @@ public class Controller { // Commit the message to the local store msg.save(mProviderContext); // Setup the rest of the message and mark it completely loaded - mLegacyController.copyOneMessageToProvider(pop3Message, msg, - Message.FLAG_LOADED_COMPLETE, mProviderContext); + Utilities.copyOneMessageToProvider(mProviderContext, pop3Message, msg, + Message.FLAG_LOADED_COMPLETE); // Restore the complete message and return it return Message.restoreMessageWithId(mProviderContext, msg.mId); } catch (MessagingException e) { @@ -356,35 +329,12 @@ public class Controller { Log.d("updateMailboxList", "RemoteException" + e); } } else { - // MessagingController implementation - mLegacyController.listFolders(accountId, mLegacyListener); + throw new IllegalStateException("No service for updateMailboxList?"); } } }); } - /** - * Request a remote update of a mailbox. For use by the timed service. - * - * 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) { - mLegacyListener.checkMailFinished(mContext, accountId, mailboxId, tag); - } else { - // MessagingController implementation - Utility.runAsync(new Runnable() { - @Override - public void run() { - mLegacyController.checkMail(accountId, tag, mLegacyListener); - } - }); - } - } - /** * Request a remote update of a mailbox. * @@ -392,7 +342,6 @@ 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); @@ -404,24 +353,9 @@ public class Controller { // is implemented for other protocols Log.d("updateMailbox", "RemoteException" + e); } - } else { - // MessagingController implementation - Utility.runAsync(new Runnable() { - @Override - public void run() { - // TODO shouldn't be passing fully-build accounts & mailboxes into APIs - Account account = - Account.restoreAccountWithId(mProviderContext, accountId); - Mailbox mailbox = - Mailbox.restoreMailboxWithId(mProviderContext, mailboxId); - if (account == null || mailbox == null || - mailbox.mType == Mailbox.TYPE_SEARCH) { - return; - } - mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); - } - }); - } + } else { + throw new IllegalStateException("No service for loadMessageForView?"); + } } /** @@ -434,40 +368,29 @@ 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) EmailServiceProxy service = getServiceForMessage(messageId); - if (service != null && service.isRemote()) { - // Get rid of this!! + if (service.isRemote()) { // 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); ContentValues cv = new ContentValues(); cv.put(MessageColumns.FLAG_LOADED, Message.FLAG_LOADED_COMPLETE); mProviderContext.getContentResolver().update(uri, cv, null, null); - Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for service-based message."); + Log.d(Logging.LOG_TAG, "Unexpected loadMessageForView() for remote service message."); final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); synchronized (mListeners) { for (Result listener : mListeners) { listener.loadMessageForViewCallback(null, accountId, messageId, 100); } } - } else if (service != null) { - // IMAP here for now + } else { try { service.loadMore(messageId); } catch (RemoteException e) { } - } else { - // MessagingController implementation - Utility.runAsync(new Runnable() { - @Override - public void run() { - mLegacyController.loadMessageForView(messageId, mLegacyListener); - } - }); } } @@ -598,50 +521,12 @@ 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 = - Account.restoreAccountWithId(mProviderContext, accountId); - if (account == null) { - return; - } - final long sentboxId = findOrCreateMailboxOfType(accountId, Mailbox.TYPE_SENT); - Utility.runAsync(new Runnable() { - @Override - public void run() { - mLegacyController.sendPendingMessages(account, sentboxId, mLegacyListener); - } - }); - } - - /** - * Try to send all pending messages for a given account - * - * @param accountId the account for which to send messages - */ - public void sendPendingMessages(long accountId) { - // 1. make sure we even have an outbox, exit early if not - final long outboxId = - Mailbox.findMailboxOfType(mProviderContext, accountId, Mailbox.TYPE_OUTBOX); - if (outboxId == Mailbox.NO_MAILBOX) { - return; - } - - // 2. dispatch as necessary - IEmailService service = getServiceForAccount(accountId); - if (service != null) { - // Service implementation - try { - service.startSync(outboxId, false); - } 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 - sendPendingMessagesSmtp(accountId); + private void sendPendingMessages(long accountId) { + EmailServiceProxy service = + EmailServiceUtils.getServiceForAccount(mContext, null, accountId); + try { + service.sendMail(accountId); + } catch (RemoteException e) { } } @@ -718,7 +603,7 @@ public class Controller { mProviderContext.getContentResolver().update(uri, cv, null, null); // Trigger a refresh using the new, longer limit mailbox.mVisibleLimit += Email.VISIBLE_LIMIT_INCREMENT; - mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); + //mLegacyController.synchronizeMailbox(account, mailbox, mLegacyListener); } }); } @@ -1005,24 +890,13 @@ public class Controller { // TODO Change exception handling to be consistent with however this method // is implemented for other protocols Log.e("searchMessages", "RemoteException", e); - return 0; } - } else { - // This is the actual mailbox we'll be searching - Mailbox actualMailbox = Mailbox.restoreMailboxWithId(mContext, searchParams.mMailboxId); - if (actualMailbox == null) { - Log.e(Logging.LOG_TAG, "Unable to find mailbox " + searchParams.mMailboxId - + " to search in with " + searchParams); - return 0; - } - // Do the search - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Search: " + searchParams.mFilter); - } - // Plumb this - return 0; - //return mLegacyController.searchMailbox(accountId, searchParams, searchMailboxId); } + return 0; + } + + private EmailServiceProxy getServiceForAccount(long accountId) { + return EmailServiceUtils.getServiceForAccount(mContext, mCallbackProxy, accountId); } /** @@ -1096,46 +970,6 @@ public class Controller { return getServiceForAccount(message.mAccountKey); } - /** - * For a given account id, return a service proxy if applicable, or null. - * - * @param accountId the message of interest - * @result service proxy, or null if n/a - */ - private EmailServiceProxy getServiceForAccount(long accountId) { - if (isMessagingController(accountId)) return null; - if (Account.getProtocol(mContext, accountId).equals(HostAuth.SCHEME_IMAP)) { - return getImapEmailService(); - } - return 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 - */ - public boolean isMessagingController(Account account) { - if (account == null) return false; - return isMessagingController(account.mId); - } - - public boolean isMessagingController(long accountId) { - Boolean isLegacyController = mLegacyControllerMap.get(accountId); - if (isLegacyController == null) { - String protocol = Account.getProtocol(mProviderContext, accountId); - isLegacyController = (HostAuth.SCHEME_POP3.equals(protocol)); - mLegacyControllerMap.put(accountId, isLegacyController); - } - return isLegacyController; - } - /** * Delete an account. */ @@ -1177,7 +1011,6 @@ public class Controller { SecurityPolicy.getInstance(context).reducePolicies(); Email.setServicesEnabledSync(context); Email.setNotifyUiAccountsChanged(true); - MailService.actionReschedule(context); } catch (Exception e) { Log.w(Logging.LOG_TAG, "Exception while deleting account", e); } @@ -1343,248 +1176,6 @@ public class Controller { } } - /** - * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and - * pass down to {@link Result}. - */ - public class MessageRetrievalListenerBridge implements MessageRetrievalListener { - private final long mMessageId; - private final long mAttachmentId; - private final long mAccountId; - - public MessageRetrievalListenerBridge(long messageId, long attachmentId) { - mMessageId = messageId; - mAttachmentId = attachmentId; - mAccountId = Account.getAccountIdForMessageId(mProviderContext, mMessageId); - } - - @Override - public void loadAttachmentProgress(int progress) { - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, - progress); - } - } - } - - @Override - public void messageRetrieved(com.android.emailcommon.mail.Message message) { - } - } - - /** - * Support for receiving callbacks from MessagingController and dealing with UI going - * out of scope. - */ - public class LegacyListener extends MessagingListener { - public LegacyListener() { - } - - @Override - public void listFoldersStarted(long accountId) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxListCallback(null, accountId, 0); - } - } - } - - @Override - public void listFoldersFailed(long accountId, String message) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxListCallback(new MessagingException(message), accountId, 0); - } - } - } - - @Override - public void listFoldersFinished(long accountId) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxListCallback(null, accountId, 100); - } - } - } - - @Override - public void synchronizeMailboxStarted(long accountId, long mailboxId) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxCallback(null, accountId, mailboxId, 0, 0, null); - } - } - } - - @Override - public void synchronizeMailboxFinished(long accountId, long mailboxId, - int totalMessagesInMailbox, int numNewMessages, ArrayList addedMessages) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxCallback(null, accountId, mailboxId, 100, numNewMessages, - addedMessages); - } - } - } - - @Override - public void synchronizeMailboxFailed(long accountId, long mailboxId, Exception e) { - MessagingException me; - if (e instanceof MessagingException) { - me = (MessagingException) e; - } else { - me = new MessagingException(e.toString()); - } - synchronized (mListeners) { - for (Result l : mListeners) { - l.updateMailboxCallback(me, accountId, mailboxId, 0, 0, null); - } - } - } - - @Override - public void checkMailStarted(Context context, long accountId, long tag) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.serviceCheckMailCallback(null, accountId, -1, 0, tag); - } - } - } - - @Override - public void checkMailFinished(Context context, long accountId, long folderId, long tag) { - synchronized (mListeners) { - for (Result l : mListeners) { - l.serviceCheckMailCallback(null, accountId, folderId, 100, tag); - } - } - } - - @Override - public void loadMessageForViewStarted(long messageId) { - final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadMessageForViewCallback(null, accountId, messageId, 0); - } - } - } - - @Override - public void loadMessageForViewFinished(long messageId) { - final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadMessageForViewCallback(null, accountId, messageId, 100); - } - } - } - - @Override - public void loadMessageForViewFailed(long messageId, String message) { - final long accountId = Account.getAccountIdForMessageId(mProviderContext, messageId); - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadMessageForViewCallback(new MessagingException(message), - accountId, messageId, 0); - } - } - } - - @Override - public void loadAttachmentStarted(long accountId, long messageId, long attachmentId, - boolean requiresDownload) { - try { - mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, - EmailServiceStatus.IN_PROGRESS, 0); - } catch (RemoteException e) { - } - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 0); - } - } - } - - @Override - public void loadAttachmentFinished(long accountId, long messageId, long attachmentId) { - try { - mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, - EmailServiceStatus.SUCCESS, 100); - } catch (RemoteException e) { - } - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.loadAttachmentCallback(null, accountId, messageId, attachmentId, 100); - } - } - } - - @Override - public void loadAttachmentFailed(long accountId, long messageId, long attachmentId, - MessagingException me, boolean background) { - try { - // If the cause of the MessagingException is an IOException, we send a status of - // CONNECTION_ERROR; in this case, AttachmentDownloadService will try again to - // download the attachment. Otherwise, the error is considered non-recoverable. - int status = EmailServiceStatus.ATTACHMENT_NOT_FOUND; - if (me != null && me.getCause() instanceof IOException) { - status = EmailServiceStatus.CONNECTION_ERROR; - } - mCallbackProxy.loadAttachmentStatus(messageId, attachmentId, status, 0); - } catch (RemoteException e) { - } - synchronized (mListeners) { - for (Result listener : mListeners) { - // TODO We are overloading the exception here. The UI listens for this - // callback and displays a toast if the exception is not null. Since we - // want to avoid displaying toast for background operations, we force - // the exception to be null. This needs to be re-worked so the UI will - // only receive (or at least pays attention to) responses for requests - // it explicitly cares about. Then we would not need to overload the - // exception parameter. - listener.loadAttachmentCallback(background ? null : me, accountId, messageId, - attachmentId, 0); - } - } - } - - @Override - synchronized public void sendPendingMessagesStarted(long accountId, long messageId) { - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.sendMailCallback(null, accountId, messageId, 0); - } - } - } - - @Override - synchronized public void sendPendingMessagesCompleted(long accountId) { - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.sendMailCallback(null, accountId, -1, 100); - } - } - } - - @Override - synchronized public void sendPendingMessagesFailed(long accountId, long messageId, - Exception reason) { - MessagingException me; - if (reason instanceof MessagingException) { - me = (MessagingException) reason; - } else { - me = new MessagingException(reason.toString()); - } - synchronized (mListeners) { - for (Result listener : mListeners) { - listener.sendMailCallback(me, accountId, messageId, 0); - } - } - } - } - /** * Service callback for service operations */ @@ -1764,8 +1355,7 @@ public class Controller { * Proxy that can be used to broadcast service callbacks; we currently use this only for * loadAttachment callbacks */ - private final IEmailServiceCallback.Stub mCallbackProxy = - new IEmailServiceCallback.Stub() { + private final IEmailServiceCallback.Stub mCallbackProxy = new IEmailServiceCallback.Stub() { /** * Broadcast a callback to the everyone that's registered @@ -1799,147 +1389,23 @@ public class Controller { } @Override - public void sendMessageStatus(long messageId, String subject, int statusCode, int progress){ + public void syncMailboxListStatus(long accountId, int statusCode, int progress) + throws RemoteException { } @Override - public void loadMessageStatus(long messageId, int statusCode, int progress){ + public void syncMailboxStatus(long mailboxId, int statusCode, int progress) + throws RemoteException { } @Override - public void syncMailboxListStatus(long accountId, int statusCode, int progress) { + public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) + throws RemoteException { } @Override - public void syncMailboxStatus(long mailboxId, int statusCode, int progress) { + public void loadMessageStatus(long messageId, int statusCode, int progress) + throws RemoteException { } }; - - public static class ControllerService extends Service { - /** - * Create our EmailService implementation here. For now, only loadAttachment is supported; - * the intention, however, is to move more functionality to the service interface - */ - 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, - attachmentId); - if (att != null) { - if (Email.DEBUG) { - Log.d(TAG, "loadAttachment " + attachmentId + ": " + att.mFileName); - } - Message msg = Message.restoreMessageWithId(ControllerService.this, - att.mMessageKey); - if (msg != null) { - // If the message is a forward and the attachment needs downloading, we need - // to retrieve the message from the source, rather than from the message - // itself - if ((msg.mFlags & Message.FLAG_TYPE_FORWARD) != 0) { - String[] cols = Utility.getRowColumns(ControllerService.this, - Body.CONTENT_URI, BODY_SOURCE_KEY_PROJECTION, WHERE_MESSAGE_KEY, - new String[] {Long.toString(msg.mId)}); - if (cols != null) { - msg = Message.restoreMessageWithId(ControllerService.this, - Long.parseLong(cols[BODY_SOURCE_KEY_COLUMN])); - if (msg == null) { - // TODO: We can try restoring from the deleted table here... - return; - } - } - } - MessagingController legacyController = sInstance.mLegacyController; - LegacyListener legacyListener = sInstance.mLegacyListener; - legacyController.loadAttachment(msg.mAccountKey, msg.mId, msg.mMailboxKey, - attachmentId, legacyListener, background); - } else { - // Send back the specific error status for this case - sInstance.mCallbackProxy.loadAttachmentStatus(att.mMessageKey, attachmentId, - EmailServiceStatus.MESSAGE_NOT_FOUND, 0); - } - } - } - - @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; - } - - @Override - public int getApiLevel() { - return Api.LEVEL; - } - }; - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - } } diff --git a/src/com/android/email/Email.java b/src/com/android/email/Email.java index c1b1f454d..970373b33 100644 --- a/src/com/android/email/Email.java +++ b/src/com/android/email/Email.java @@ -28,7 +28,6 @@ import com.android.email.activity.MessageCompose; import com.android.email.activity.ShortcutPicker; import com.android.email.service.AttachmentDownloadService; import com.android.email.service.MailService; -import com.android.email.widget.WidgetConfiguration; import com.android.emailcommon.Logging; import com.android.emailcommon.TempDirectory; import com.android.emailcommon.provider.Account; @@ -127,15 +126,6 @@ public class Email extends Application { private static void setServicesEnabled(Context context, boolean enabled) { PackageManager pm = context.getPackageManager(); - if (!enabled && pm.getComponentEnabledSetting( - new ComponentName(context, MailService.class)) == - PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - /* - * If no accounts now exist but the service is still enabled we're about to disable it - * so we'll reschedule to kill off any existing alarms. - */ - MailService.actionReschedule(context); - } pm.setComponentEnabledSetting( new ComponentName(context, MessageCompose.class), enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : @@ -156,15 +146,6 @@ public class Email extends Application { enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED : PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); - if (enabled && pm.getComponentEnabledSetting( - new ComponentName(context, MailService.class)) == - PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { - /* - * And now if accounts do exist then we've just enabled the service and we want to - * schedule alarms for the new accounts. - */ - MailService.actionReschedule(context); - } // Start/stop the various services depending on whether there are any accounts startOrStopService(enabled, context, new Intent(context, AttachmentDownloadService.class)); diff --git a/src/com/android/email/MessagingController.java b/src/com/android/email/MessagingController.java deleted file mode 100644 index 27c5eee45..000000000 --- a/src/com/android/email/MessagingController.java +++ /dev/null @@ -1,1393 +0,0 @@ -/* - * Copyright (C) 2008 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; - -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; -import android.net.TrafficStats; -import android.net.Uri; -import android.os.Process; -import android.text.TextUtils; -import android.util.Log; - -import com.android.email.mail.Sender; -import com.android.email.mail.Store; -import com.android.emailcommon.Logging; -import com.android.emailcommon.TrafficFlags; -import com.android.emailcommon.internet.MimeBodyPart; -import com.android.emailcommon.internet.MimeHeader; -import com.android.emailcommon.internet.MimeMultipart; -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.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.Attachment; -import com.android.emailcommon.provider.EmailContent.AttachmentColumns; -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.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.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.LinkedBlockingQueue; - -/** - * Starts a long running (application) Thread that will run through commands - * that require remote mailbox access. This class is used to serialize and - * prioritize these commands. Each method that will submit a command requires a - * MessagingListener instance to be provided. It is expected that that listener - * has also been added as a registered listener using addListener(). When a - * command is to be executed, if the listener that was provided with the command - * is no longer registered the command is skipped. The design idea for the above - * is that when an Activity starts it registers as a listener. When it is paused - * it removes itself. Thus, any commands that that activity submitted are - * removed from the queue once the activity is no longer active. - */ -public class MessagingController implements Runnable { - - /** - * The maximum message size that we'll consider to be "small". A small message is downloaded - * in full immediately instead of in pieces. Anything over this size will be downloaded in - * pieces with attachments being left off completely and downloaded on demand. - * - * - * 25k for a "small" message was picked by educated trial and error. - * http://answers.google.com/answers/threadview?id=312463 claims that the - * average size of an email is 59k, which I feel is too large for our - * blind download. The following tests were performed on a download of - * 25 random messages. - *

-     * 5k - 61 seconds,
-     * 25k - 51 seconds,
-     * 55k - 53 seconds,
-     * 
- * So 25k gives good performance and a reasonable data footprint. Sounds good to me. - */ - private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); - - /** - * We write this into the serverId field of messages that will never be upsynced. - */ - private static final String LOCAL_SERVERID_PREFIX = "Local-"; - - private static final ContentValues PRUNE_ATTACHMENT_CV = new ContentValues(); - static { - PRUNE_ATTACHMENT_CV.putNull(AttachmentColumns.CONTENT_URI); - } - - private static MessagingController sInstance = null; - private final BlockingQueue mCommands = new LinkedBlockingQueue(); - private final Thread mThread; - - /** - * All access to mListeners *must* be synchronized - */ - private final GroupMessagingListener mListeners = new GroupMessagingListener(); - private boolean mBusy; - private final Context mContext; - private final Controller mController; - - /** - * Simple cache for last search result mailbox by account and serverId, since the most common - * case will be repeated use of the same mailbox - */ - private long mLastSearchAccountKey = Account.NO_ACCOUNT; - private String mLastSearchServerId = null; - private Mailbox mLastSearchRemoteMailbox = null; - - protected MessagingController(Context _context, Controller _controller) { - mContext = _context.getApplicationContext(); - mController = _controller; - mThread = new Thread(this); - mThread.start(); - } - - /** - * Gets or creates the singleton instance of MessagingController. Application is used to - * provide a Context to classes that need it. - */ - public synchronized static MessagingController getInstance(Context _context, - Controller _controller) { - if (sInstance == null) { - sInstance = new MessagingController(_context, _controller); - } - return sInstance; - } - - /** - * Inject a mock controller. Used only for testing. Affects future calls to getInstance(). - */ - public static void injectMockController(MessagingController mockController) { - sInstance = mockController; - } - - // TODO: seems that this reading of mBusy isn't thread-safe - public boolean isBusy() { - return mBusy; - } - - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - // TODO: add an end test to this infinite loop - while (true) { - Command command; - try { - command = mCommands.take(); - } catch (InterruptedException e) { - continue; //re-test the condition on the eclosing while - } - if (command.listener == null || isActiveListener(command.listener)) { - mBusy = true; - command.runnable.run(); - mListeners.controllerCommandCompleted(mCommands.size() > 0); - } - mBusy = false; - } - } - - private void put(String description, MessagingListener listener, Runnable runnable) { - try { - Command command = new Command(); - command.listener = listener; - command.runnable = runnable; - command.description = description; - mCommands.add(command); - } - catch (IllegalStateException ie) { - throw new Error(ie); - } - } - - public void addListener(MessagingListener listener) { - mListeners.addListener(listener); - } - - public void removeListener(MessagingListener listener) { - mListeners.removeListener(listener); - } - - private boolean isActiveListener(MessagingListener listener) { - return mListeners.isActiveListener(listener); - } - - private static final int MAILBOX_COLUMN_ID = 0; - private static final int MAILBOX_COLUMN_SERVER_ID = 1; - private static final int MAILBOX_COLUMN_TYPE = 2; - - /** Small projection for just the columns required for a sync. */ - private static final String[] MAILBOX_PROJECTION = new String[] { - MailboxColumns.ID, - MailboxColumns.SERVER_ID, - MailboxColumns.TYPE, - }; - - /** - * Synchronize the folder list with the remote server. Synchronization occurs in the - * background and results are passed through the {@link MessagingListener}. If the - * given listener is not {@code null}, it must have been previously added to the set - * of listeners using the {@link #addListener(MessagingListener)}. Otherwise, no - * actions will be performed. - * - * TODO this needs to cache the remote folder list - * TODO break out an inner listFoldersSynchronized which could simplify checkMail - * - * @param accountId ID of the account for which to list the folders - * @param listener A listener to notify - */ - void listFolders(final long accountId, MessagingListener listener) { - final Account account = Account.restoreAccountWithId(mContext, accountId); - if (account == null) { - Log.i(Logging.LOG_TAG, "Could not load account id " + accountId - + ". Has it been removed?"); - return; - } - mListeners.listFoldersStarted(accountId); - put("listFolders", listener, new Runnable() { - // TODO For now, mailbox addition occurs in the server-dependent store implementation, - // but, mailbox removal occurs here. Instead, each store should be responsible for - // content synchronization (addition AND removal) since each store will likely need - // to implement it's own, unique synchronization methodology. - @Override - public void run() { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); - Cursor localFolderCursor = null; - try { - // Step 1: Get remote mailboxes - Store store = Store.getInstance(account, mContext); - Folder[] remoteFolders = store.updateFolders(); - HashSet remoteFolderNames = new HashSet(); - for (int i = 0, count = remoteFolders.length; i < count; i++) { - remoteFolderNames.add(remoteFolders[i].getName()); - } - - // Step 2: Get local mailboxes - localFolderCursor = mContext.getContentResolver().query( - Mailbox.CONTENT_URI, - MAILBOX_PROJECTION, - EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", - new String[] { String.valueOf(account.mId) }, - null); - - // Step 3: Remove any local mailbox not on the remote list - while (localFolderCursor.moveToNext()) { - String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); - // Short circuit if we have a remote mailbox with the same name - if (remoteFolderNames.contains(mailboxPath)) { - continue; - } - - int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); - long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); - switch (mailboxType) { - case Mailbox.TYPE_INBOX: - case Mailbox.TYPE_DRAFTS: - case Mailbox.TYPE_OUTBOX: - case Mailbox.TYPE_SENT: - case Mailbox.TYPE_TRASH: - case Mailbox.TYPE_SEARCH: - // Never, ever delete special mailboxes - break; - default: - // Drop all attachment files related to this mailbox - AttachmentUtilities.deleteAllMailboxAttachmentFiles( - mContext, accountId, mailboxId); - // Delete the mailbox; database triggers take care of related - // Message, Body and Attachment records - Uri uri = ContentUris.withAppendedId( - Mailbox.CONTENT_URI, mailboxId); - mContext.getContentResolver().delete(uri, null, null); - break; - } - } - mListeners.listFoldersFinished(accountId); - } catch (Exception e) { - mListeners.listFoldersFailed(accountId, e.toString()); - } finally { - if (localFolderCursor != null) { - localFolderCursor.close(); - } - } - } - }); - } - - /** - * Start background synchronization of the specified folder. - * @param account - * @param folder - * @param listener - */ - public void synchronizeMailbox(final Account account, - final Mailbox folder, MessagingListener listener) { - /* - * We don't ever sync the Outbox. - */ - if (folder.mType == Mailbox.TYPE_OUTBOX) { - return; - } - mListeners.synchronizeMailboxStarted(account.mId, folder.mId); - put("synchronizeMailbox", listener, new Runnable() { - @Override - public void run() { - synchronizeMailboxSynchronous(account, folder); - } - }); - } - - /** - * 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 - */ - private void synchronizeMailboxSynchronous(final Account account, - final Mailbox folder) { - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); - mListeners.synchronizeMailboxStarted(account.mId, folder.mId); - if ((folder.mFlags & Mailbox.FLAG_HOLDS_MAIL) == 0) { - // We don't hold messages, so, nothing to synchronize - mListeners.synchronizeMailboxFinished(account.mId, folder.mId, 0, 0, null); - return; - } - NotificationController nc = NotificationController.getInstance(mContext); - try { - - // Select generic sync or store-specific sync - SyncResults results = synchronizeMailboxGeneric(account, folder); - // The account might have been deleted - if (results == null) return; - mListeners.synchronizeMailboxFinished(account.mId, folder.mId, - results.mTotalMessages, - results.mAddedMessages.size(), - results.mAddedMessages); - // 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); - } - mListeners.synchronizeMailboxFailed(account.mId, folder.mId, 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 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 - */ - void loadUnsyncedMessages(final Account account, Folder remoteFolder, - ArrayList unsyncedMessages, final Mailbox toMailbox) - throws MessagingException { - - // 1. Divide the unsynced messages into small & large (by size) - - // TODO doing this work here (synchronously) is problematic because it prevents the UI - // from affecting the order (e.g. download a message because the user requested it.) Much - // of this logic should move out to a different sync loop that attempts to update small - // groups of messages at a time, as a background task. However, we can't just return - // (yet) because POP messages don't have an envelope yet.... - - ArrayList largeMessages = new ArrayList(); - ArrayList smallMessages = new ArrayList(); - for (Message message : unsyncedMessages) { - if (message.getSize() > (MAX_SMALL_MESSAGE_SIZE)) { - largeMessages.add(message); - } else { - smallMessages.add(message); - } - } - - // 2. Download small messages - - // TODO Problems with this implementation. 1. For IMAP, where we get a real envelope, - // this is going to be inefficient and duplicate work we've already done. 2. It's going - // back to the DB for a local message that we already had (and discarded). - - // For small messages, we specify "body", which returns everything (incl. attachments) - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.fetch(smallMessages.toArray(new Message[smallMessages.size()]), fp, - new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(message, account, toMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - // 3. Download large messages. We ask the server to give us the message structure, - // but not all of the attachments. - fp.clear(); - fp.add(FetchProfile.Item.STRUCTURE); - remoteFolder.fetch(largeMessages.toArray(new Message[largeMessages.size()]), fp, null); - for (Message message : largeMessages) { - if (message.getBody() == null) { - // POP doesn't support STRUCTURE mode, so we'll just do a partial download - // (hopefully enough to see some/all of the body) and mark the message for - // further download. - fp.clear(); - fp.add(FetchProfile.Item.BODY_SANE); - // TODO a good optimization here would be to make sure that all Stores set - // the proper size after this fetch and compare the before and after size. If - // they equal we can mark this SYNCHRONIZED instead of PARTIALLY_SYNCHRONIZED - remoteFolder.fetch(new Message[] { message }, fp, null); - - // Store the partially-loaded message and mark it partially loaded - copyOneMessageToProvider(message, account, toMailbox, - EmailContent.Message.FLAG_LOADED_PARTIAL); - } else { - // We have a structure to deal with, from which - // we can pull down the parts we want to actually store. - // Build a list of parts we are interested in. Text parts will be downloaded - // right now, attachments will be left for later. - ArrayList viewables = new ArrayList(); - ArrayList attachments = new ArrayList(); - MimeUtility.collectParts(message, viewables, attachments); - // Download the viewables immediately - for (Part part : viewables) { - fp.clear(); - fp.add(part); - // TODO what happens if the network connection dies? We've got partial - // messages with incorrect status stored. - remoteFolder.fetch(new Message[] { message }, fp, null); - } - // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(message, account, toMailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - } - } - - } - - public void downloadFlagAndEnvelope(final Account account, final Mailbox mailbox, - Folder remoteFolder, ArrayList unsyncedMessages, - HashMap localMessageMap, final ArrayList unseenMessages) - throws MessagingException { - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.FLAGS); - fp.add(FetchProfile.Item.ENVELOPE); - - final HashMap localMapCopy; - if (localMessageMap != null) - localMapCopy = new HashMap(localMessageMap); - else { - localMapCopy = new HashMap(); - } - - remoteFolder.fetch(unsyncedMessages.toArray(new Message[0]), fp, - new MessageRetrievalListener() { - @Override - public void messageRetrieved(Message message) { - try { - // Determine if the new message was already known (e.g. partial) - // And create or reload the full message info - LocalMessageInfo localMessageInfo = - localMapCopy.get(message.getUid()); - EmailContent.Message localMessage = null; - if (localMessageInfo == null) { - localMessage = new EmailContent.Message(); - } else { - localMessage = EmailContent.Message.restoreMessageWithId( - mContext, localMessageInfo.mId); - } - - if (localMessage != null) { - try { - // Copy the fields that are available into the message - LegacyConversions.updateMessageFields(localMessage, - message, account.mId, mailbox.mId); - // Commit the message to the local store - saveOrUpdate(localMessage, mContext); - // Track the "new" ness of the downloaded message - if (!message.isSet(Flag.SEEN) && unseenMessages != null) { - unseenMessages.add(localMessage.mId); - } - } catch (MessagingException me) { - Log.e(Logging.LOG_TAG, - "Error while copying downloaded message." + me); - } - - } - } - catch (Exception e) { - Log.e(Logging.LOG_TAG, - "Error while storing downloaded message." + e.toString()); - } - } - - @Override - public void loadAttachmentProgress(int progress) { - } - }); - - } - - /** - * Generic synchronizer - used for POP3 and 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 SyncResults synchronizeMailboxGeneric(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 = mContext.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(mContext, 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, mContext); - // 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(); - - int newMessageCount = 0; - 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()); - if (localMessage == null) { - newMessageCount++; - } - // 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(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(mContext, 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(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 void copyOneMessageToProvider(Message message, Account account, - Mailbox folder, int loadStatus) { - EmailContent.Message localMessage = null; - Cursor c = null; - try { - c = mContext.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(message, localMessage, loadStatus, mContext); - } - } 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 void copyOneMessageToProvider(Message message, EmailContent.Message localMessage, - int loadStatus, Context context) { - 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()); - } - } - - /** - * 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 - * the local message. - * - * @param remoteStore the remote store we're working in - * @param account The account in which we are working - * @param newMailbox The mailbox we're appending to - * @param message The message we're appending - * @return true if successfully uploaded - */ - private boolean processPendingAppend(Store remoteStore, Account account, - Mailbox newMailbox, EmailContent.Message message) - throws MessagingException { - - boolean updateInternalDate = false; - boolean updateMessage = false; - boolean deleteMessage = false; - - // 1. Find the remote folder that we're appending to and create and/or open it - Folder remoteFolder = remoteStore.getFolder(newMailbox.mServerId); - if (!remoteFolder.exists()) { - if (!remoteFolder.canCreate(FolderType.HOLDS_MESSAGES)) { - // This is POP3, we cannot actually upload. Instead, we'll update the message - // locally with a fake serverId (so we don't keep trying here) and return. - if (message.mServerId == null || message.mServerId.length() == 0) { - message.mServerId = LOCAL_SERVERID_PREFIX + message.mId; - Uri uri = - ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, message.mServerId); - mContext.getContentResolver().update(uri, cv, null, null); - } - return true; - } - if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - // This is a (hopefully) transient error and we return false to try again later - return false; - } - } - remoteFolder.open(OpenMode.READ_WRITE); - if (remoteFolder.getMode() != OpenMode.READ_WRITE) { - return false; - } - - // 2. If possible, load a remote message with the matching UID - Message remoteMessage = null; - if (message.mServerId != null && message.mServerId.length() > 0) { - remoteMessage = remoteFolder.getMessage(message.mServerId); - } - - // 3. If a remote message could not be found, upload our local message - if (remoteMessage == null) { - // 3a. Create a legacy message to upload - Message localMessage = LegacyConversions.makeMessage(mContext, message); - - // 3b. Upload it - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.appendMessages(new Message[] { localMessage }); - - // 3b. And record the UID from the server - message.mServerId = localMessage.getUid(); - updateInternalDate = true; - updateMessage = true; - } else { - // 4. If the remote message exists we need to determine which copy to keep. - FetchProfile fp = new FetchProfile(); - fp.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage }, fp, null); - Date localDate = new Date(message.mServerTimeStamp); - Date remoteDate = remoteMessage.getInternalDate(); - if (remoteDate != null && remoteDate.compareTo(localDate) > 0) { - // 4a. If the remote message is newer than ours we'll just - // delete ours and move on. A sync will get the server message - // if we need to be able to see it. - deleteMessage = true; - } else { - // 4b. Otherwise we'll upload our message and then delete the remote message. - - // Create a legacy message to upload - Message localMessage = LegacyConversions.makeMessage(mContext, message); - - // 4c. Upload it - fp.clear(); - fp = new FetchProfile(); - fp.add(FetchProfile.Item.BODY); - remoteFolder.appendMessages(new Message[] { localMessage }); - - // 4d. Record the UID and new internalDate from the server - message.mServerId = localMessage.getUid(); - updateInternalDate = true; - updateMessage = true; - - // 4e. And delete the old copy of the message from the server - remoteMessage.setFlag(Flag.DELETED, true); - } - } - - // 5. If requested, Best-effort to capture new "internaldate" from the server - if (updateInternalDate && message.mServerId != null) { - try { - Message remoteMessage2 = remoteFolder.getMessage(message.mServerId); - if (remoteMessage2 != null) { - FetchProfile fp2 = new FetchProfile(); - fp2.add(FetchProfile.Item.ENVELOPE); - remoteFolder.fetch(new Message[] { remoteMessage2 }, fp2, null); - message.mServerTimeStamp = remoteMessage2.getInternalDate().getTime(); - updateMessage = true; - } - } catch (MessagingException me) { - // skip it - we can live without this - } - } - - // 6. Perform required edits to local copy of message - if (deleteMessage || updateMessage) { - Uri uri = ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, message.mId); - ContentResolver resolver = mContext.getContentResolver(); - if (deleteMessage) { - resolver.delete(uri, null, null); - } else if (updateMessage) { - ContentValues cv = new ContentValues(); - cv.put(EmailContent.Message.SERVER_ID, message.mServerId); - cv.put(EmailContent.Message.SERVER_TIMESTAMP, message.mServerTimeStamp); - resolver.update(uri, cv, null, null); - } - } - - return true; - } - - /** - * Finish loading a message that have been partially downloaded. - * - * @param messageId the message to load - * @param listener the callback by which results will be reported - */ - public void loadMessageForView(final long messageId, MessagingListener listener) { - mListeners.loadMessageForViewStarted(messageId); - put("loadMessageForViewRemote", listener, new Runnable() { - @Override - public void run() { - try { - // 1. Resample the message, in case it disappeared or synced while - // this command was in queue - EmailContent.Message message = - EmailContent.Message.restoreMessageWithId(mContext, messageId); - if (message == null) { - mListeners.loadMessageForViewFailed(messageId, "Unknown message"); - return; - } - if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { - mListeners.loadMessageForViewFinished(messageId); - return; - } - - // 2. Open the remote folder. - // TODO combine with common code in loadAttachment - Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); - Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); - if (account == null || mailbox == null) { - mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); - return; - } - TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); - - Store remoteStore = Store.getInstance(account, mContext); - 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(remoteMessage, account, mailbox, - EmailContent.Message.FLAG_LOADED_COMPLETE); - - // 5. Notify UI - mListeners.loadMessageForViewFinished(messageId); - - } catch (MessagingException me) { - if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); - mListeners.loadMessageForViewFailed(messageId, me.getMessage()); - } catch (RuntimeException rte) { - mListeners.loadMessageForViewFailed(messageId, rte.getMessage()); - } - } - }); - } - - /** - * Attempts to load the attachment specified by id from the given account and message. - */ - public void loadAttachment(final long accountId, final long messageId, final long mailboxId, - final long attachmentId, MessagingListener listener, final boolean background) { - mListeners.loadAttachmentStarted(accountId, messageId, attachmentId, true); - - put("loadAttachment", listener, new Runnable() { - @Override - public void run() { - try { - //1. Check if the attachment is already here and return early in that case - Attachment attachment = - Attachment.restoreAttachmentWithId(mContext, attachmentId); - if (attachment == null) { - mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, - new MessagingException("The attachment is null"), - background); - return; - } - if (Utility.attachmentExists(mContext, attachment)) { - mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); - return; - } - - // 2. Open the remote folder. - // TODO all of these could be narrower projections - Account account = Account.restoreAccountWithId(mContext, accountId); - Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); - EmailContent.Message message = - EmailContent.Message.restoreMessageWithId(mContext, messageId); - - if (account == null || mailbox == null || message == null) { - mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, - new MessagingException( - "Account, mailbox, message or attachment are null"), - background); - return; - } - TrafficStats.setThreadStatsTag( - TrafficFlags.getAttachmentFlags(mContext, account)); - - Store remoteStore = Store.getInstance(account, mContext); - Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); - remoteFolder.open(OpenMode.READ_WRITE); - - // 3. Generate a shell message in which to retrieve the attachment, - // and a shell BodyPart for the attachment. Then glue them together. - Message storeMessage = remoteFolder.createMessage(message.mServerId); - MimeBodyPart storePart = new MimeBodyPart(); - storePart.setSize((int)attachment.mSize); - storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, - attachment.mLocation); - storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, - String.format("%s;\n name=\"%s\"", - attachment.mMimeType, - attachment.mFileName)); - // TODO is this always true for attachments? I think we dropped the - // true encoding along the way - storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); - - MimeMultipart multipart = new MimeMultipart(); - multipart.setSubType("mixed"); - multipart.addBodyPart(storePart); - - storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); - storeMessage.setBody(multipart); - - // 4. Now ask for the attachment to be fetched - FetchProfile fp = new FetchProfile(); - fp.add(storePart); - remoteFolder.fetch(new Message[] { storeMessage }, fp, - mController.new MessageRetrievalListenerBridge( - messageId, attachmentId)); - - // If we failed to load the attachment, throw an Exception here, so that - // AttachmentDownloadService knows that we failed - if (storePart.getBody() == null) { - throw new MessagingException("Attachment not loaded."); - } - - // 5. Save the downloaded file and update the attachment as necessary - LegacyConversions.saveAttachmentBody(mContext, storePart, attachment, - accountId); - - // 6. Report success - mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); - } - catch (MessagingException me) { - if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); - mListeners.loadAttachmentFailed( - accountId, messageId, attachmentId, me, background); - } catch (IOException ioe) { - Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); - } - }}); - } - - /** - * Attempt to send any messages that are sitting in the Outbox. - * @param account - * @param listener - */ - public void sendPendingMessages(final Account account, final long sentFolderId, - MessagingListener listener) { - put("sendPendingMessages", listener, new Runnable() { - @Override - public void run() { - sendPendingMessagesSynchronous(account, sentFolderId); - } - }); - } - - /** - * Attempt to send all messages sitting in the given account's outbox. Optionally, - * if the server requires it, the message will be moved to the given sent folder. - */ - public void sendPendingMessagesSynchronous(final Account account, - long sentFolderId) { - TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account)); - NotificationController nc = NotificationController.getInstance(mContext); - // 1. Loop through all messages in the account's outbox - long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); - if (outboxId == Mailbox.NO_MAILBOX) { - return; - } - ContentResolver resolver = mContext.getContentResolver(); - Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, - EmailContent.Message.ID_COLUMN_PROJECTION, - EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, - null); - try { - // 2. exit early - if (c.getCount() <= 0) { - return; - } - // 3. do one-time setup of the Sender & other stuff - mListeners.sendPendingMessagesStarted(account.mId, -1); - - Sender sender = Sender.getInstance(mContext, account); - Store remoteStore = Store.getInstance(account, mContext); - boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); - ContentValues moveToSentValues = null; - if (requireMoveMessageToSentFolder) { - moveToSentValues = new ContentValues(); - moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolderId); - } - - // 4. loop through the available messages and send them - while (c.moveToNext()) { - long messageId = -1; - try { - messageId = c.getLong(0); - mListeners.sendPendingMessagesStarted(account.mId, messageId); - // Don't send messages with unloaded attachments - if (Utility.hasUnloadedAttachments(mContext, messageId)) { - if (Email.DEBUG) { - Log.d(Logging.LOG_TAG, "Can't send #" + messageId + - "; unloaded attachments"); - } - continue; - } - sender.sendMessage(messageId); - } catch (MessagingException me) { - // report error for this message, but keep trying others - if (me instanceof AuthenticationFailedException) { - nc.showLoginFailedNotification(account.mId); - } - mListeners.sendPendingMessagesFailed(account.mId, messageId, me); - continue; - } - // 5. move to sent, or delete - Uri syncedUri = - ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); - if (requireMoveMessageToSentFolder) { - // If this is a forwarded message and it has attachments, delete them, as they - // duplicate information found elsewhere (on the server). This saves storage. - EmailContent.Message msg = - EmailContent.Message.restoreMessageWithId(mContext, messageId); - if (msg != null && - ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { - AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, - messageId); - } - resolver.update(syncedUri, moveToSentValues, null, null); - } else { - AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, - messageId); - Uri uri = - ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); - resolver.delete(uri, null, null); - resolver.delete(syncedUri, null, null); - } - } - // 6. report completion/success - mListeners.sendPendingMessagesCompleted(account.mId); - nc.cancelLoginFailedNotification(account.mId); - } catch (MessagingException me) { - if (me instanceof AuthenticationFailedException) { - nc.showLoginFailedNotification(account.mId); - } - mListeners.sendPendingMessagesFailed(account.mId, -1, me); - } finally { - c.close(); - } - } - - /** - * Checks mail for an account. - * This entry point is for use by the mail checking service only, because it - * gives slightly different callbacks (so the service doesn't get confused by callbacks - * triggered by/for the foreground UI. - * - * TODO clean up the execution model which is unnecessarily threaded due to legacy code - * - * @param accountId the account to check - * @param listener - */ - public void checkMail(final long accountId, final long tag, final MessagingListener listener) { - mListeners.checkMailStarted(mContext, accountId, tag); - - // This puts the command on the queue (not synchronous) - listFolders(accountId, null); - - // Put this on the queue as well so it follows listFolders - put("checkMail", listener, new Runnable() { - @Override - public void run() { - // send any pending outbound messages. note, there is a slight race condition - // here if we somehow don't have a sent folder, but this should never happen - // because the call to sendMessage() would have built one previously. - long inboxId = -1; - Account account = Account.restoreAccountWithId(mContext, accountId); - if (account != null) { - long sentboxId = Mailbox.findMailboxOfType(mContext, accountId, - Mailbox.TYPE_SENT); - if (sentboxId != Mailbox.NO_MAILBOX) { - sendPendingMessagesSynchronous(account, sentboxId); - } - // find mailbox # for inbox and sync it. - // TODO we already know this in Controller, can we pass it in? - inboxId = Mailbox.findMailboxOfType(mContext, accountId, Mailbox.TYPE_INBOX); - if (inboxId != Mailbox.NO_MAILBOX) { - Mailbox mailbox = - Mailbox.restoreMailboxWithId(mContext, inboxId); - if (mailbox != null) { - synchronizeMailboxSynchronous(account, mailbox); - } - } - } - mListeners.checkMailFinished(mContext, accountId, inboxId, tag); - } - }); - } - - private static class Command { - public Runnable runnable; - - public MessagingListener listener; - - public String description; - - @Override - public String toString() { - return description; - } - } - - /** 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; - } - } -} diff --git a/src/com/android/email/activity/setup/DebugFragment.java b/src/com/android/email/activity/setup/DebugFragment.java index f7cd8d31c..d2b8409fe 100644 --- a/src/com/android/email/activity/setup/DebugFragment.java +++ b/src/com/android/email/activity/setup/DebugFragment.java @@ -21,7 +21,6 @@ import com.android.email.Preferences; import com.android.email.R; import com.android.email.activity.UiUtilities; import com.android.email.service.EmailServiceUtils; -import com.android.email.service.MailService; import com.android.emailcommon.Logging; import android.app.Fragment; @@ -44,7 +43,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener, private CheckBox mEnableExchangeLoggingView; private CheckBox mEnableExchangeFileLoggingView; private CheckBox mInhibitGraphicsAccelerationView; - private CheckBox mForceOneMinuteRefreshView; private CheckBox mEnableStrictModeView; private Preferences mPreferences; @@ -92,11 +90,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener, mInhibitGraphicsAccelerationView.setChecked(Email.sDebugInhibitGraphicsAcceleration); mInhibitGraphicsAccelerationView.setOnCheckedChangeListener(this); - mForceOneMinuteRefreshView = (CheckBox) - UiUtilities.getView(view, R.id.debug_force_one_minute_refresh); - mForceOneMinuteRefreshView.setChecked(mPreferences.getForceOneMinuteRefresh()); - mForceOneMinuteRefreshView.setOnCheckedChangeListener(this); - mEnableStrictModeView = (CheckBox) UiUtilities.getView(view, R.id.debug_enable_strict_mode); mEnableStrictModeView.setChecked(mPreferences.getEnableStrictMode()); @@ -125,10 +118,6 @@ public class DebugFragment extends Fragment implements OnCheckedChangeListener, Email.sDebugInhibitGraphicsAcceleration = isChecked; mPreferences.setInhibitGraphicsAcceleration(isChecked); break; - case R.id.debug_force_one_minute_refresh: - mPreferences.setForceOneMinuteRefresh(isChecked); - MailService.actionReschedule(getActivity()); - break; case R.id.debug_enable_strict_mode: mPreferences.setEnableStrictMode(isChecked); Email.enableStrictMode(isChecked); diff --git a/src/com/android/email/provider/Utilities.java b/src/com/android/email/provider/Utilities.java new file mode 100644 index 000000000..ab175f666 --- /dev/null +++ b/src/com/android/email/provider/Utilities.java @@ -0,0 +1,150 @@ +/* + * 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.provider; + +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.util.Log; + +import com.android.email.LegacyConversions; +import com.android.emailcommon.Logging; +import com.android.emailcommon.internet.MimeUtility; +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.MessageColumns; +import com.android.emailcommon.provider.EmailContent.SyncColumns; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.utility.ConversionUtilities; + +import java.io.IOException; +import java.util.ArrayList; + +public class Utilities { + /** + * 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()); + } + } + + public static void saveOrUpdate(EmailContent content, Context context) { + if (content.isSaved()) { + content.update(context, content.toContentValues()); + } else { + content.save(context); + } + } + +} diff --git a/src/com/android/email/service/AttachmentDownloadService.java b/src/com/android/email/service/AttachmentDownloadService.java index b2defe52d..29ffbb665 100644 --- a/src/com/android/email/service/AttachmentDownloadService.java +++ b/src/com/android/email/service/AttachmentDownloadService.java @@ -33,7 +33,6 @@ import android.text.format.DateUtils; import android.util.Log; import com.android.email.AttachmentInfo; -import com.android.email.Controller.ControllerService; import com.android.email.Email; import com.android.email.EmailConnectivityManager; import com.android.email.NotificationController; @@ -450,8 +449,8 @@ public class AttachmentDownloadService extends Service implements Runnable { * @return whether or not the download was started */ /*package*/ synchronized boolean tryStartDownload(DownloadRequest req) { - Intent intent = getServiceIntentForAccount(req.accountId); - if (intent == null) return false; + EmailServiceProxy service = EmailServiceUtils.getServiceForAccount( + AttachmentDownloadService.this, mServiceCallback, req.accountId); // Do not download the same attachment multiple times boolean alreadyInProgress = mDownloadsInProgress.get(req.attachmentId) != null; @@ -461,7 +460,7 @@ public class AttachmentDownloadService extends Service implements Runnable { if (Email.DEBUG) { Log.d(TAG, ">> Starting download for attachment #" + req.attachmentId); } - startDownload(intent, req); + startDownload(service, req); } catch (RemoteException e) { // TODO: Consider whether we need to do more in this case... // For now, fix up our data to reflect the failure @@ -478,18 +477,16 @@ public class AttachmentDownloadService extends Service implements Runnable { * Do the work of starting an attachment download using the EmailService interface, and * set our watchdog alarm * - * @param serviceClass the class that will attempt the download + * @param serviceClass the service handling the download * @param req the DownloadRequest * @throws RemoteException */ - private void startDownload(Intent intent, DownloadRequest req) + private void startDownload(EmailServiceProxy service, DownloadRequest req) throws RemoteException { req.startTime = System.currentTimeMillis(); req.inProgress = true; mDownloadsInProgress.put(req.attachmentId, req); - EmailServiceProxy proxy = - new EmailServiceProxy(mContext, intent, mServiceCallback); - proxy.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND); + service.loadAttachment(req.attachmentId, req.priority != PRIORITY_FOREGROUND); // Lazily initialize our (reusable) pending intent if (mWatchdogPendingIntent == null) { createWatchdogPendingIntent(mContext); @@ -685,11 +682,6 @@ public class AttachmentDownloadService extends Service implements Runnable { } } - @Override - public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) - throws RemoteException { - } - @Override public void syncMailboxListStatus(long accountId, int statusCode, int progress) throws RemoteException { @@ -700,33 +692,17 @@ public class AttachmentDownloadService extends Service implements Runnable { throws RemoteException { } + @Override + public void sendMessageStatus(long messageId, String subject, int statusCode, int progress) + throws RemoteException { + } + @Override public void loadMessageStatus(long messageId, int statusCode, int progress) throws RemoteException { } } - /** - * Return an Intent to be used used based on the account type of the provided account id. We - * cache the results to avoid repeated database access - * @param accountId the id of the account - * @return the Intent to be used for the account or null (if the account no longer exists) - */ - private synchronized Intent getServiceIntentForAccount(long accountId) { - // TODO: We should have some more data-driven way of determining the service intent. - Intent serviceIntent = mAccountServiceMap.get(accountId); - if (serviceIntent == null) { - String protocol = Account.getProtocol(mContext, accountId); - if (protocol == null) return null; - serviceIntent = new Intent(mContext, ControllerService.class); - if (protocol.equals("eas")) { - serviceIntent = new Intent(EmailServiceProxy.EXCHANGE_INTENT); - } - mAccountServiceMap.put(accountId, serviceIntent); - } - return serviceIntent; - } - /*package*/ void addServiceIntentForTest(long accountId, Intent intent) { mAccountServiceMap.put(accountId, intent); } diff --git a/src/com/android/email/service/EmailBroadcastProcessorService.java b/src/com/android/email/service/EmailBroadcastProcessorService.java index bfd4f3f33..6c41ebb80 100644 --- a/src/com/android/email/service/EmailBroadcastProcessorService.java +++ b/src/com/android/email/service/EmailBroadcastProcessorService.java @@ -29,7 +29,6 @@ import android.database.Cursor; import android.net.Uri; import android.util.Log; -import com.android.email.Email; import com.android.email.Preferences; import com.android.email.SecurityPolicy; import com.android.email.VendorPolicyLoader; @@ -106,15 +105,7 @@ public class EmailBroadcastProcessorService extends IntentService { if (Intent.ACTION_BOOT_COMPLETED.equals(broadcastAction)) { onBootCompleted(); - - // TODO: Do a better job when we get ACTION_DEVICE_STORAGE_LOW. - // The code below came from very old code.... - } else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(broadcastAction)) { - // Stop IMAP/POP3 poll. - MailService.actionCancel(this); - } else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(broadcastAction)) { - enableComponentsIfNecessary(); - } else if (ACTION_SECRET_CODE.equals(broadcastAction) + } else if (ACTION_SECRET_CODE.equals(broadcastAction) && SECRET_CODE_HOST_DEBUG_SCREEN.equals(broadcastIntent.getData().getHost())) { AccountSettings.actionSettingsWithDebug(this); } else if (AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION.equals(broadcastAction)) { @@ -126,22 +117,12 @@ public class EmailBroadcastProcessorService extends IntentService { } } - private void enableComponentsIfNecessary() { - if (Email.setServicesEnabledSync(this)) { - // At least one account exists. - // TODO probably we should check if it's a POP/IMAP account. - MailService.actionReschedule(this); - } - } - /** * Handles {@link Intent#ACTION_BOOT_COMPLETED}. Called on a worker thread. */ private void onBootCompleted() { performOneTimeInitialization(); - enableComponentsIfNecessary(); - // Starts the service for Exchange, if supported. EmailServiceUtils.startExchangeService(this); } diff --git a/src/com/android/email/service/EmailServiceStub.java b/src/com/android/email/service/EmailServiceStub.java new file mode 100644 index 000000000..21c9a9557 --- /dev/null +++ b/src/com/android/email/service/EmailServiceStub.java @@ -0,0 +1,517 @@ +/* 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.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.TrafficStats; +import android.net.Uri; +import android.os.Bundle; +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.Controller.Result; +import com.android.email.mail.Sender; +import com.android.email.mail.Store; +import com.android.email.provider.Utilities; +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.MimeBodyPart; +import com.android.emailcommon.internet.MimeHeader; +import com.android.emailcommon.internet.MimeMultipart; +import com.android.emailcommon.mail.AuthenticationFailedException; +import com.android.emailcommon.mail.FetchProfile; +import com.android.emailcommon.mail.Folder; +import com.android.emailcommon.mail.Message; +import com.android.emailcommon.mail.MessagingException; +import com.android.emailcommon.mail.Folder.MessageRetrievalListener; +import com.android.emailcommon.mail.Folder.OpenMode; +import com.android.emailcommon.provider.Account; +import com.android.emailcommon.provider.EmailContent; +import com.android.emailcommon.provider.HostAuth; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.provider.EmailContent.Attachment; +import com.android.emailcommon.provider.EmailContent.MailboxColumns; +import com.android.emailcommon.provider.EmailContent.MessageColumns; +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.Utility; + +import java.io.IOException; +import java.util.HashSet; + +/** + * EmailServiceStub is an abstract class representing an EmailService + * + * This class provides legacy support for a few methods that are common to both + * IMAP and POP3, including startSync, loadMore, loadAttachment, and sendMail + */ +public abstract class EmailServiceStub extends IEmailService.Stub implements IEmailService { + + private static final int MAILBOX_COLUMN_ID = 0; + private static final int MAILBOX_COLUMN_SERVER_ID = 1; + private static final int MAILBOX_COLUMN_TYPE = 2; + + /** Small projection for just the columns required for a sync. */ + private static final String[] MAILBOX_PROJECTION = new String[] { + MailboxColumns.ID, + MailboxColumns.SERVER_ID, + MailboxColumns.TYPE, + }; + + private Context mContext; + private IEmailServiceCallback.Stub mCallback; + + protected void init(Context context, IEmailServiceCallback.Stub callbackProxy) { + mContext = context; + mCallback = callbackProxy; + } + + @Override + public Bundle validate(HostAuth hostauth) throws RemoteException { + // TODO Auto-generated method stub + return null; + } + + @Override + public void startSync(long mailboxId, boolean userRequest) throws RemoteException { + Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, mailboxId); + if (mailbox == null) return; + Account account = Account.restoreAccountWithId(mContext, mailbox.mAccountKey); + if (account == null) return; + android.accounts.Account acct = new android.accounts.Account(account.mEmailAddress, + AccountManagerTypes.TYPE_POP_IMAP); + 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 { + // Not required + } + + @Override + public void loadMore(long messageId) throws RemoteException { + // Load a message for view... + try { + // 1. Resample the message, in case it disappeared or synced while + // this command was in queue + EmailContent.Message message = + EmailContent.Message.restoreMessageWithId(mContext, messageId); + if (message == null) { + mCallback.loadMessageStatus(messageId, + EmailServiceStatus.MESSAGE_NOT_FOUND, 0); + return; + } + if (message.mFlagLoaded == EmailContent.Message.FLAG_LOADED_COMPLETE) { + // We should NEVER get here + mCallback.loadMessageStatus(messageId, 0, 100); + return; + } + + // 2. Open the remote folder. + // TODO combine with common code in loadAttachment + Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); + Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); + if (account == null || mailbox == null) { + //mListeners.loadMessageForViewFailed(messageId, "null account or mailbox"); + return; + } + TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); + + Store remoteStore = Store.getInstance(account, mContext); + 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 + Utilities.copyOneMessageToProvider(mContext, remoteMessage, account, mailbox, + EmailContent.Message.FLAG_LOADED_COMPLETE); + + // 5. Notify UI + mCallback.loadMessageStatus(messageId, 0, 100); + + } catch (MessagingException me) { + if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); + mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); + } catch (RuntimeException rte) { + mCallback.loadMessageStatus(messageId, EmailServiceStatus.REMOTE_EXCEPTION, 0); + } + } + + @Override + public void loadAttachment(long attachmentId, boolean background) throws RemoteException { + try { + //1. Check if the attachment is already here and return early in that case + Attachment attachment = + Attachment.restoreAttachmentWithId(mContext, attachmentId); + if (attachment == null) { +// mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, +// new MessagingException("The attachment is null"), +// background); + return; + } + if (Utility.attachmentExists(mContext, attachment)) { +// mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); + return; + } + EmailContent.Message message = + EmailContent.Message.restoreMessageWithId(mContext, attachment.mMessageKey); + + // 2. Open the remote folder. + // TODO all of these could be narrower projections + Account account = Account.restoreAccountWithId(mContext, message.mAccountKey); + Mailbox mailbox = Mailbox.restoreMailboxWithId(mContext, message.mMailboxKey); + + if (account == null || mailbox == null || message == null) { +// mListeners.loadAttachmentFailed(accountId, messageId, attachmentId, +// new MessagingException( +// "Account, mailbox, message or attachment are null"), +// background); + return; + } + TrafficStats.setThreadStatsTag( + TrafficFlags.getAttachmentFlags(mContext, account)); + + Store remoteStore = Store.getInstance(account, mContext); + Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); + remoteFolder.open(OpenMode.READ_WRITE); + + // 3. Generate a shell message in which to retrieve the attachment, + // and a shell BodyPart for the attachment. Then glue them together. + Message storeMessage = remoteFolder.createMessage(message.mServerId); + MimeBodyPart storePart = new MimeBodyPart(); + storePart.setSize((int)attachment.mSize); + storePart.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, + attachment.mLocation); + storePart.setHeader(MimeHeader.HEADER_CONTENT_TYPE, + String.format("%s;\n name=\"%s\"", + attachment.mMimeType, + attachment.mFileName)); + // TODO is this always true for attachments? I think we dropped the + // true encoding along the way + storePart.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64"); + + MimeMultipart multipart = new MimeMultipart(); + multipart.setSubType("mixed"); + multipart.addBodyPart(storePart); + + storeMessage.setHeader(MimeHeader.HEADER_CONTENT_TYPE, "multipart/mixed"); + storeMessage.setBody(multipart); + + // 4. Now ask for the attachment to be fetched + FetchProfile fp = new FetchProfile(); + fp.add(storePart); + remoteFolder.fetch(new Message[] { storeMessage }, fp, + new MessageRetrievalListenerBridge(message.mId, attachmentId)); + + // If we failed to load the attachment, throw an Exception here, so that + // AttachmentDownloadService knows that we failed + if (storePart.getBody() == null) { + throw new MessagingException("Attachment not loaded."); + } + + // 5. Save the downloaded file and update the attachment as necessary + LegacyConversions.saveAttachmentBody(mContext, storePart, attachment, + message.mAccountKey); + + // 6. Report success +// mListeners.loadAttachmentFinished(accountId, messageId, attachmentId); + } + catch (MessagingException me) { + if (Logging.LOGD) Log.v(Logging.LOG_TAG, "", me); +// mListeners.loadAttachmentFailed( +// accountId, messageId, attachmentId, me, background); + } catch (IOException ioe) { + Log.e(Logging.LOG_TAG, "Error while storing attachment." + ioe.toString()); + } + + } + + // TODO: Implement callback + @Override + public void updateFolderList(long accountId) throws RemoteException { + Account account = Account.restoreAccountWithId(mContext, accountId); + if (account == null) return; + TrafficStats.setThreadStatsTag(TrafficFlags.getSyncFlags(mContext, account)); + Cursor localFolderCursor = null; + try { + // Step 1: Get remote mailboxes + Store store = Store.getInstance(account, mContext); + Folder[] remoteFolders = store.updateFolders(); + HashSet remoteFolderNames = new HashSet(); + for (int i = 0, count = remoteFolders.length; i < count; i++) { + remoteFolderNames.add(remoteFolders[i].getName()); + } + + // Step 2: Get local mailboxes + localFolderCursor = mContext.getContentResolver().query( + Mailbox.CONTENT_URI, + MAILBOX_PROJECTION, + EmailContent.MailboxColumns.ACCOUNT_KEY + "=?", + new String[] { String.valueOf(account.mId) }, + null); + + // Step 3: Remove any local mailbox not on the remote list + while (localFolderCursor.moveToNext()) { + String mailboxPath = localFolderCursor.getString(MAILBOX_COLUMN_SERVER_ID); + // Short circuit if we have a remote mailbox with the same name + if (remoteFolderNames.contains(mailboxPath)) { + continue; + } + + int mailboxType = localFolderCursor.getInt(MAILBOX_COLUMN_TYPE); + long mailboxId = localFolderCursor.getLong(MAILBOX_COLUMN_ID); + switch (mailboxType) { + case Mailbox.TYPE_INBOX: + case Mailbox.TYPE_DRAFTS: + case Mailbox.TYPE_OUTBOX: + case Mailbox.TYPE_SENT: + case Mailbox.TYPE_TRASH: + case Mailbox.TYPE_SEARCH: + // Never, ever delete special mailboxes + break; + default: + // Drop all attachment files related to this mailbox + AttachmentUtilities.deleteAllMailboxAttachmentFiles( + mContext, accountId, mailboxId); + // Delete the mailbox; database triggers take care of related + // Message, Body and Attachment records + Uri uri = ContentUris.withAppendedId( + Mailbox.CONTENT_URI, mailboxId); + mContext.getContentResolver().delete(uri, null, null); + break; + } + } + //mListeners.listFoldersFinished(accountId); + } catch (Exception e) { + //mListeners.listFoldersFailed(accountId, e.toString()); + } finally { + if (localFolderCursor != null) { + localFolderCursor.close(); + } + } + } + + @Override + public boolean createFolder(long accountId, String name) throws RemoteException { + // Not required + return false; + } + + @Override + public boolean deleteFolder(long accountId, String name) throws RemoteException { + // Not required + return false; + } + + @Override + public boolean renameFolder(long accountId, String oldName, String newName) + throws RemoteException { + // Not required + return false; + } + + @Override + public void setCallback(IEmailServiceCallback cb) throws RemoteException { + // Not required + } + + @Override + public void setLogging(int on) throws RemoteException { + // Not required + } + + @Override + public void hostChanged(long accountId) throws RemoteException { + // Not required + } + + @Override + public Bundle autoDiscover(String userName, String password) throws RemoteException { + // Not required + return null; + } + + @Override + public void sendMeetingResponse(long messageId, int response) throws RemoteException { + // Not required + } + + @Override + public void deleteAccountPIMData(long accountId) throws RemoteException { + // Not required + } + + @Override + public int getApiLevel() throws RemoteException { + return Api.LEVEL; + } + + @Override + public int searchMessages(long accountId, SearchParams params, long destMailboxId) + throws RemoteException { + // Not required + return 0; + } + + @Override + public void sendMail(long accountId) throws RemoteException { + Account account = Account.restoreAccountWithId(mContext, accountId); + TrafficStats.setThreadStatsTag(TrafficFlags.getSmtpFlags(mContext, account)); + NotificationController nc = NotificationController.getInstance(mContext); + // 1. Loop through all messages in the account's outbox + long outboxId = Mailbox.findMailboxOfType(mContext, account.mId, Mailbox.TYPE_OUTBOX); + if (outboxId == Mailbox.NO_MAILBOX) { + return; + } + ContentResolver resolver = mContext.getContentResolver(); + Cursor c = resolver.query(EmailContent.Message.CONTENT_URI, + EmailContent.Message.ID_COLUMN_PROJECTION, + EmailContent.Message.MAILBOX_KEY + "=?", new String[] { Long.toString(outboxId) }, + null); + try { + // 2. exit early + if (c.getCount() <= 0) { + return; + } + // 3. do one-time setup of the Sender & other stuff + //mListeners.sendPendingMessagesStarted(account.mId, -1); + + Sender sender = Sender.getInstance(mContext, account); + Store remoteStore = Store.getInstance(account, mContext); + boolean requireMoveMessageToSentFolder = remoteStore.requireCopyMessageToSentFolder(); + ContentValues moveToSentValues = null; + if (requireMoveMessageToSentFolder) { + Mailbox sentFolder = + Mailbox.restoreMailboxOfType(mContext, accountId, Mailbox.TYPE_SENT); + moveToSentValues = new ContentValues(); + moveToSentValues.put(MessageColumns.MAILBOX_KEY, sentFolder.mId); + } + + // 4. loop through the available messages and send them + while (c.moveToNext()) { + long messageId = -1; + try { + messageId = c.getLong(0); + //mListeners.sendPendingMessagesStarted(account.mId, messageId); + // Don't send messages with unloaded attachments + if (Utility.hasUnloadedAttachments(mContext, messageId)) { + if (Email.DEBUG) { + Log.d(Logging.LOG_TAG, "Can't send #" + messageId + + "; unloaded attachments"); + } + continue; + } + sender.sendMessage(messageId); + } catch (MessagingException me) { + // report error for this message, but keep trying others + if (me instanceof AuthenticationFailedException) { + nc.showLoginFailedNotification(account.mId); + } + //mListeners.sendPendingMessagesFailed(account.mId, messageId, me); + continue; + } + // 5. move to sent, or delete + Uri syncedUri = + ContentUris.withAppendedId(EmailContent.Message.SYNCED_CONTENT_URI, messageId); + if (requireMoveMessageToSentFolder) { + // If this is a forwarded message and it has attachments, delete them, as they + // duplicate information found elsewhere (on the server). This saves storage. + EmailContent.Message msg = + EmailContent.Message.restoreMessageWithId(mContext, messageId); + if (msg != null && + ((msg.mFlags & EmailContent.Message.FLAG_TYPE_FORWARD) != 0)) { + AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, + messageId); + } + resolver.update(syncedUri, moveToSentValues, null, null); + } else { + AttachmentUtilities.deleteAllAttachmentFiles(mContext, account.mId, + messageId); + Uri uri = + ContentUris.withAppendedId(EmailContent.Message.CONTENT_URI, messageId); + resolver.delete(uri, null, null); + resolver.delete(syncedUri, null, null); + } + } + // 6. report completion/success + //mListeners.sendPendingMessagesCompleted(account.mId); + nc.cancelLoginFailedNotification(account.mId); + } catch (MessagingException me) { + if (me instanceof AuthenticationFailedException) { + nc.showLoginFailedNotification(account.mId); + } + //mListeners.sendPendingMessagesFailed(account.mId, -1, me); + } finally { + c.close(); + } + } + + /** + * Bridge to intercept {@link MessageRetrievalListener#loadAttachmentProgress} and + * pass down to {@link Result}. + */ + public class MessageRetrievalListenerBridge implements MessageRetrievalListener { + private final long mMessageId; +// private final long mAttachmentId; +// private final long mAccountId; + + public MessageRetrievalListenerBridge(long messageId, long attachmentId) { + mMessageId = messageId; +// mAttachmentId = attachmentId; +// mAccountId = Account.getAccountIdForMessageId(mContext, mMessageId); + } + + @Override + public void loadAttachmentProgress(int progress) { +// synchronized (mListeners) { +// for (Result listener : mListeners) { +// listener.loadAttachmentCallback(null, mAccountId, mMessageId, mAttachmentId, +// progress); +// } +// } + } + + @Override + public void messageRetrieved(com.android.emailcommon.mail.Message message) { + } + } +} diff --git a/src/com/android/email/service/EmailServiceUtils.java b/src/com/android/email/service/EmailServiceUtils.java index d76325b2d..2d14dce49 100644 --- a/src/com/android/email/service/EmailServiceUtils.java +++ b/src/com/android/email/service/EmailServiceUtils.java @@ -16,19 +16,13 @@ package com.android.email.service; -import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.Bundle; -import android.os.IBinder; -import android.os.RemoteException; - -import com.android.emailcommon.Api; +import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.HostAuth; import com.android.emailcommon.service.EmailServiceProxy; import com.android.emailcommon.service.IEmailService; import com.android.emailcommon.service.IEmailServiceCallback; -import com.android.emailcommon.service.SearchParams; /** * Utility functions for EmailService support. @@ -74,106 +68,32 @@ public class EmailServiceUtils { return new EmailServiceProxy(context, ImapService.class, callback); } + public static EmailServiceProxy getPop3Service(Context context, + IEmailServiceCallback callback) { + return new EmailServiceProxy(context, Pop3Service.class, callback); + } + public static boolean isExchangeAvailable(Context context) { return isServiceAvailable(context, EmailServiceProxy.EXCHANGE_INTENT); } /** - * An empty {@link IEmailService} implementation which is used instead of - * {@link com.android.exchange.ExchangeService} on the build with no exchange support. + * For a given account id, return a service proxy if applicable, or null. * - *

In theory, the service in question isn't used on the no-exchange-support build, - * because we won't have any exchange accounts in that case, so we wouldn't have to have this - * class. However, there are a few places we do use the service even if there's no exchange - * accounts (e.g. setLogging), so this class is added for safety and simplicity. + * @param accountId the message of interest + * @result service proxy, or null if n/a */ - 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; - } - - @Override - public IBinder onBind(Intent intent) { - return null; + public static EmailServiceProxy getServiceForAccount(Context context, + IEmailServiceCallback callback, long accountId) { + String protocol = Account.getProtocol(context, accountId); + if (protocol.equals(HostAuth.SCHEME_IMAP)) { + return getImapService(context, callback); + } else if (protocol.equals(HostAuth.SCHEME_POP3)) { + return getPop3Service(context, callback); + } else if (protocol.equals(HostAuth.SCHEME_EAS)) { + return getExchangeService(context, callback); + } else { + throw new IllegalArgumentException("Account with unknown protocol: " + accountId); } } } diff --git a/src/com/android/email/service/ImapService.java b/src/com/android/email/service/ImapService.java index 892c40b39..0182c6e2e 100644 --- a/src/com/android/email/service/ImapService.java +++ b/src/com/android/email/service/ImapService.java @@ -25,7 +25,6 @@ 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; @@ -36,8 +35,7 @@ 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.email.provider.Utilities; import com.android.emailcommon.Logging; import com.android.emailcommon.TrafficFlags; import com.android.emailcommon.internet.MimeUtility; @@ -57,16 +55,10 @@ 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; @@ -81,6 +73,14 @@ public class ImapService extends Service { private static final Flag[] FLAG_LIST_FLAGGED = new Flag[] { Flag.FLAGGED }; private static final Flag[] FLAG_LIST_ANSWERED = new Flag[] { Flag.ANSWERED }; + /** + * Simple cache for last search result mailbox by account and serverId, since the most common + * case will be repeated use of the same mailbox + */ + private static long mLastSearchAccountKey = Account.NO_ACCOUNT; + private static String mLastSearchServerId = null; + private static Mailbox mLastSearchRemoteMailbox = null; + /** * 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 @@ -203,164 +203,27 @@ public class ImapService extends Service { /** * 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; - } + private final EmailServiceStub mBinder = new EmailServiceStub() { @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) { + try { + return searchMailboxImpl(getApplicationContext(), accountId, searchParams, + destMailboxId); + } catch (MessagingException e) { + } return 0; } - }; @Override public IBinder onBind(Intent intent) { + mBinder.init(this, sCallbackProxy); return mBinder; } @@ -380,11 +243,7 @@ public class ImapService extends Service { 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; + synchronizeMailboxGeneric(context, account, folder); // Clear authentication notification for this account nc.cancelLoginFailedNotification(account.mId); } catch (MessagingException e) { @@ -436,14 +295,6 @@ public class ImapService extends Service { } } - 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 @@ -488,7 +339,7 @@ public class ImapService extends Service { @Override public void messageRetrieved(Message message) { // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(context, message, account, toMailbox, + Utilities.copyOneMessageToProvider(context, message, account, toMailbox, EmailContent.Message.FLAG_LOADED_COMPLETE); } @@ -515,7 +366,7 @@ public class ImapService extends Service { remoteFolder.fetch(new Message[] { message }, fp, null); // Store the partially-loaded message and mark it partially loaded - copyOneMessageToProvider(context, message, account, toMailbox, + Utilities.copyOneMessageToProvider(context, message, account, toMailbox, EmailContent.Message.FLAG_LOADED_PARTIAL); } else { // We have a structure to deal with, from which @@ -534,7 +385,7 @@ public class ImapService extends Service { remoteFolder.fetch(new Message[] { message }, fp, null); } // Store the updated message locally and mark it fully loaded - copyOneMessageToProvider(context, message, account, toMailbox, + Utilities.copyOneMessageToProvider(context, message, account, toMailbox, EmailContent.Message.FLAG_LOADED_COMPLETE); } } @@ -579,7 +430,7 @@ public class ImapService extends Service { LegacyConversions.updateMessageFields(localMessage, message, account.mId, mailbox.mId); // Commit the message to the local store - saveOrUpdate(localMessage, context); + Utilities.saveOrUpdate(localMessage, context); // Track the "new" ness of the downloaded message if (!message.isSet(Flag.SEEN) && unseenMessages != null) { unseenMessages.add(localMessage.mId); @@ -614,7 +465,7 @@ public class ImapService extends Service { * @return results of the sync pass * @throws MessagingException */ - private static SyncResults synchronizeMailboxGeneric(final Context context, + private static void synchronizeMailboxGeneric(final Context context, final Account account, final Mailbox mailbox) throws MessagingException { /* @@ -630,8 +481,7 @@ public class ImapService extends Service { // 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); + return; } // 1. Get the message list from the local store and create an index of the uids @@ -664,7 +514,7 @@ public class ImapService extends Service { Store remoteStore = Store.getInstance(account, context); // The account might have been deleted - if (remoteStore == null) return null; + if (remoteStore == null) return; Folder remoteFolder = remoteStore.getFolder(mailbox.mServerId); /* @@ -678,7 +528,7 @@ public class ImapService extends Service { || mailbox.mType == Mailbox.TYPE_DRAFTS) { if (!remoteFolder.exists()) { if (!remoteFolder.create(FolderType.HOLDS_MESSAGES)) { - return new SyncResults(0, unseenMessages); + return; } } } @@ -829,107 +679,6 @@ public class ImapService extends Service { // 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()); - } } /** @@ -975,10 +724,10 @@ public class ImapService extends Service { if (!TextUtils.isEmpty(message.mProtocolSearchInfo)) { long accountKey = message.mAccountKey; String protocolSearchInfo = message.mProtocolSearchInfo; -// if (accountKey == mLastSearchAccountKey && -// protocolSearchInfo.equals(mLastSearchServerId)) { -// return mLastSearchRemoteMailbox; -// } + 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)}, @@ -987,9 +736,9 @@ public class ImapService extends Service { if (c.moveToNext()) { Mailbox mailbox = new Mailbox(); mailbox.restore(c); -// mLastSearchAccountKey = accountKey; -// mLastSearchServerId = protocolSearchInfo; -// mLastSearchRemoteMailbox = mailbox; + mLastSearchAccountKey = accountKey; + mLastSearchServerId = protocolSearchInfo; + mLastSearchRemoteMailbox = mailbox; return mailbox; } else { return null; @@ -1574,22 +1323,6 @@ public class ImapService extends Service { 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 */ @@ -1686,7 +1419,7 @@ public class ImapService extends Service { LegacyConversions.updateMessageFields(localMessage, message, account.mId, mailbox.mId); // Commit the message to the local store - saveOrUpdate(localMessage, context); + Utilities.saveOrUpdate(localMessage, context); localMessage.mMailboxKey = destMailboxId; // We load 50k or so; maybe it's complete, maybe not... int flag = EmailContent.Message.FLAG_LOADED_COMPLETE; @@ -1697,7 +1430,7 @@ public class ImapService extends Service { if (message.getSize() > Store.FETCH_BODY_SANE_SUGGESTED_SIZE) { flag = EmailContent.Message.FLAG_LOADED_PARTIAL; } - copyOneMessageToProvider(context, message, localMessage, flag); + Utilities.copyOneMessageToProvider(context, message, localMessage, flag); } catch (MessagingException me) { Log.e(Logging.LOG_TAG, "Error while copying downloaded message." + me); diff --git a/src/com/android/email/service/MailService.java b/src/com/android/email/service/MailService.java index 9065e67fc..c52dea91c 100644 --- a/src/com/android/email/service/MailService.java +++ b/src/com/android/email/service/MailService.java @@ -18,99 +18,37 @@ package com.android.email.service; import android.accounts.AccountManager; import android.accounts.AccountManagerCallback; -import android.app.AlarmManager; -import android.app.PendingIntent; import android.app.Service; -import android.content.ContentResolver; -import android.content.ContentUris; import android.content.Context; import android.content.Intent; -import android.content.SyncStatusObserver; import android.database.Cursor; -import android.net.Uri; import android.os.Bundle; import android.os.IBinder; -import android.os.SystemClock; -import android.text.TextUtils; import android.util.Log; import com.android.email.Controller; import com.android.email.Email; -import com.android.email.Preferences; import com.android.email.SingleRunningTask; import com.android.email.provider.AccountReconciler; import com.android.emailcommon.AccountManagerTypes; -import com.android.emailcommon.mail.MessagingException; import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; import com.android.emailcommon.provider.HostAuth; -import com.android.emailcommon.provider.Mailbox; import com.android.emailcommon.utility.EmailAsyncTask; import com.google.common.annotations.VisibleForTesting; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; /** - * Background service for refreshing non-push email accounts. - * - * TODO: Convert to IntentService to move *all* work off the UI thread, serialize work, and avoid - * possible problems with out-of-order startId processing. + * Legacy service, now used mainly for account reconciliation */ public class MailService extends Service { private static final String LOG_TAG = "Email-MailService"; - private static final String ACTION_CHECK_MAIL = - "com.android.email.intent.action.MAIL_SERVICE_WAKEUP"; - private static final String ACTION_RESCHEDULE = - "com.android.email.intent.action.MAIL_SERVICE_RESCHEDULE"; - private static final String ACTION_CANCEL = - "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 EXTRA_ACCOUNT = "com.android.email.intent.extra.ACCOUNT"; - private static final String EXTRA_ACCOUNT_INFO = "com.android.email.intent.extra.ACCOUNT_INFO"; - private static final String EXTRA_DEBUG_WATCHDOG = "com.android.email.intent.extra.WATCHDOG"; - - /** Time between watchdog checks; in milliseconds */ - private static final long WATCHDOG_DELAY = 10 * 60 * 1000; // 10 minutes - - /** Sentinel value asking to update mSyncReports if it's currently empty */ - @VisibleForTesting - static final int SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY = -1; - /** Sentinel value asking that mSyncReports be rebuilt */ - @VisibleForTesting - static final int SYNC_REPORTS_RESET = -2; - - @VisibleForTesting - Controller mController; - private final Controller.Result mControllerCallback = new ControllerResults(); - private ContentResolver mContentResolver; - private Context mContext; - - private int mStartId; - - /** - * Access must be synchronized, because there are accesses from the Controller callback - */ - /*package*/ static HashMap mSyncReports = - new HashMap(); - - public static void actionReschedule(Context context) { - Intent i = new Intent(); - i.setClass(context, MailService.class); - i.setAction(MailService.ACTION_RESCHEDULE); - context.startService(i); - } - - public static void actionCancel(Context context) { - Intent i = new Intent(); - i.setClass(context, MailService.class); - i.setAction(MailService.ACTION_CANCEL); - context.startService(i); - } /** * Entry point for AttachmentDownloadService to ask that pending mail be sent @@ -136,104 +74,21 @@ public class MailService extends Service { } }); - // TODO this needs to be passed through the controller and back to us - mStartId = startId; String action = intent.getAction(); final long accountId = intent.getLongExtra(EXTRA_ACCOUNT, -1); - mController = Controller.getInstance(this); - mController.addResultCallback(mControllerCallback); - mContentResolver = getContentResolver(); - mContext = this; - - final AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - - if ((ACTION_CHECK_MAIL).equals(action)) { - // DB access required to satisfy this intent, so offload from UI thread - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - // If we have the data, restore the last-sync-times for each account - // These are cached in the wakeup intent in case the process was killed. - restoreSyncReports(intent); - - // Sync a specific account if given - if (Email.DEBUG) { - Log.d(LOG_TAG, "action: check mail for id=" + accountId); - } - if (accountId >= 0) { - setWatchdog(accountId, alarmManager); - } - - // Start sync if account is given && auto-sync is allowed - boolean syncStarted = false; - if (accountId != -1 && ContentResolver.getMasterSyncAutomatically()) { - synchronized(mSyncReports) { - for (AccountSyncReport report: mSyncReports.values()) { - if (report.accountId == accountId) { - // 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); - } - break; - } - } - } - } - - // Reschedule if we didn't start sync. - if (!syncStarted) { - // Prevent runaway on the current account by pretending it updated - if (accountId != -1) { - updateAccountReport(accountId, 0); - } - // Find next account to sync, and reschedule - reschedule(alarmManager); - // Stop the service, unless actually syncing (which will stop the service) - stopSelf(startId); - } - } - }); - } - else if (ACTION_CANCEL.equals(action)) { - if (Email.DEBUG) { - Log.d(LOG_TAG, "action: cancel"); - } - cancel(); - stopSelf(startId); - } - else if (ACTION_SEND_PENDING_MAIL.equals(action)) { + if (ACTION_SEND_PENDING_MAIL.equals(action)) { if (Email.DEBUG) { Log.d(LOG_TAG, "action: send pending mail"); } EmailAsyncTask.runAsyncParallel(new Runnable() { @Override public void run() { - mController.sendPendingMessages(accountId); + Controller.getInstance(getApplicationContext()).sendPendingMessages(accountId); } }); stopSelf(startId); } - else if (ACTION_RESCHEDULE.equals(action)) { - if (Email.DEBUG) { - Log.d(LOG_TAG, "action: reschedule"); - } - // DB access required to satisfy this intent, so offload from UI thread - EmailAsyncTask.runAsyncParallel(new Runnable() { - @Override - public void run() { - // When called externally, we refresh the sync reports table to pick up - // any changes in the account list or account settings - refreshSyncReports(); - // Finally, scan for the next needing update, and set an alarm for it - reschedule(alarmManager); - stopSelf(startId); - } - }); - } // Returning START_NOT_STICKY means that if a mail check is killed (e.g. due to memory // pressure, there will be no explicit restart. This is OK; Note that we set a watchdog @@ -247,390 +102,6 @@ public class MailService extends Service { return null; } - @Override - public void onDestroy() { - super.onDestroy(); - Controller.getInstance(getApplication()).removeResultCallback(mControllerCallback); - } - - private void cancel() { - AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - PendingIntent pi = createAlarmIntent(-1, null, false); - alarmMgr.cancel(pi); - } - - /** - * Refresh the sync reports, to pick up any changes in the account list or account settings. - */ - private void refreshSyncReports() { - synchronized (mSyncReports) { - // Make shallow copy of sync reports so we can recover the prev sync times - HashMap oldSyncReports = - new HashMap(mSyncReports); - - // Delete the sync reports to force a refresh from live account db data - setupSyncReportsLocked(SYNC_REPORTS_RESET, this); - - // Restore prev-sync & next-sync times for any reports in the new list - for (AccountSyncReport newReport : mSyncReports.values()) { - AccountSyncReport oldReport = oldSyncReports.get(newReport.accountId); - if (oldReport != null) { - newReport.prevSyncTime = oldReport.prevSyncTime; - newReport.setNextSyncTime(); - } - } - } - } - - /** - * Create and send an alarm with the entire list. This also sends a list of known last-sync - * times with the alarm, so if we are killed between alarms, we don't lose this info. - * - * @param alarmMgr passed in so we can mock for testing. - */ - private void reschedule(AlarmManager alarmMgr) { - // restore the reports if lost - setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); - synchronized (mSyncReports) { - int numAccounts = mSyncReports.size(); - long[] accountInfo = new long[numAccounts * 2]; // pairs of { accountId, lastSync } - int accountInfoIndex = 0; - - long nextCheckTime = Long.MAX_VALUE; - AccountSyncReport nextAccount = null; - long timeNow = SystemClock.elapsedRealtime(); - - for (AccountSyncReport report : mSyncReports.values()) { - if (report.syncInterval <= 0) { // no timed checks - skip - continue; - } - long prevSyncTime = report.prevSyncTime; - long nextSyncTime = report.nextSyncTime; - - // select next account to sync - if ((prevSyncTime == 0) || (nextSyncTime < timeNow)) { // never checked, or overdue - nextCheckTime = 0; - nextAccount = report; - } else if (nextSyncTime < nextCheckTime) { // next to be checked - nextCheckTime = nextSyncTime; - nextAccount = report; - } - // collect last-sync-times for all accounts - // this is using pairs of {long,long} to simplify passing in a bundle - accountInfo[accountInfoIndex++] = report.accountId; - accountInfo[accountInfoIndex++] = report.prevSyncTime; - } - - // Clear out any unused elements in the array - while (accountInfoIndex < accountInfo.length) { - accountInfo[accountInfoIndex++] = -1; - } - - // set/clear alarm as needed - long idToCheck = (nextAccount == null) ? -1 : nextAccount.accountId; - PendingIntent pi = createAlarmIntent(idToCheck, accountInfo, false); - - if (nextAccount == null) { - alarmMgr.cancel(pi); - if (Email.DEBUG) { - Log.d(LOG_TAG, "reschedule: alarm cancel - no account to check"); - } - } else { - alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); - if (Email.DEBUG) { - Log.d(LOG_TAG, "reschedule: alarm set at " + nextCheckTime - + " for " + nextAccount); - } - } - } - } - - /** - * Create a watchdog alarm and set it. This is used in case a mail check fails (e.g. we are - * killed by the system due to memory pressure.) Normally, a mail check will complete and - * the watchdog will be replaced by the call to reschedule(). - * @param accountId the account we were trying to check - * @param alarmMgr system alarm manager - */ - private void setWatchdog(long accountId, AlarmManager alarmMgr) { - PendingIntent pi = createAlarmIntent(accountId, null, true); - long timeNow = SystemClock.elapsedRealtime(); - long nextCheckTime = timeNow + WATCHDOG_DELAY; - alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextCheckTime, pi); - } - - /** - * Return a pending intent for use by this alarm. Most of the fields must be the same - * (in order for the intent to be recognized by the alarm manager) but the extras can - * be different, and are passed in here as parameters. - */ - private PendingIntent createAlarmIntent(long checkId, long[] accountInfo, boolean isWatchdog) { - Intent i = new Intent(); - i.setClass(this, MailService.class); - i.setAction(ACTION_CHECK_MAIL); - i.putExtra(EXTRA_ACCOUNT, checkId); - i.putExtra(EXTRA_ACCOUNT_INFO, accountInfo); - if (isWatchdog) { - i.putExtra(EXTRA_DEBUG_WATCHDOG, true); - } - PendingIntent pi = PendingIntent.getService(this, 0, i, PendingIntent.FLAG_UPDATE_CURRENT); - return pi; - } - - /** - * Start a controller sync for a specific account - * - * @param controller The controller to do the sync work - * @param checkAccountId the account Id to try and check - * @param startId the id of this service launch - * @return true if mail checking has started, false if it could not (e.g. bad account id) - */ - private boolean syncOneAccount(Controller controller, long checkAccountId, int startId) { - long inboxId = Mailbox.findMailboxOfType(this, checkAccountId, Mailbox.TYPE_INBOX); - if (inboxId == Mailbox.NO_MAILBOX) { - return false; - } else { - controller.serviceCheckMail(checkAccountId, inboxId, startId); - return true; - } - } - - /** - * Note: Times are relative to SystemClock.elapsedRealtime() - * - * TODO: Look more closely at syncEnabled and see if we can simply coalesce it into - * syncInterval (e.g. if !syncEnabled, set syncInterval to -1). - */ - @VisibleForTesting - static class AccountSyncReport { - long accountId; - /** The time of the last sync, or, {@code 0}, the last sync time is unknown. */ - long prevSyncTime; - /** The time of the next sync. If {@code 0}, sync ASAP. If {@code 1}, don't sync. */ - long nextSyncTime; - /** Minimum time between syncs; in minutes. */ - int syncInterval; - /** If {@code true}, auto sync is enabled. */ - boolean syncEnabled; - - /** - * Sets the next sync time using the previous sync time and sync interval. - */ - private void setNextSyncTime() { - if (syncInterval > 0 && prevSyncTime != 0) { - nextSyncTime = prevSyncTime + (syncInterval * 1000 * 60); - } - } - - @Override - public String toString() { - return "id=" + accountId + " prevSync=" + prevSyncTime + " nextSync=" + nextSyncTime; - } - } - - /** - * scan accounts to create a list of { acct, prev sync, next sync, #new } - * use this to create a fresh copy. assumes all accounts need sync - * - * @param accountId -1 will rebuild the list if empty. other values will force loading - * of a single account (e.g if it was created after the original list population) - */ - private void setupSyncReports(long accountId) { - synchronized (mSyncReports) { - setupSyncReportsLocked(accountId, mContext); - } - } - - /** - * Handle the work of setupSyncReports. Must be synchronized on mSyncReports. - */ - @VisibleForTesting - void setupSyncReportsLocked(long accountId, Context context) { - ContentResolver resolver = context.getContentResolver(); - if (accountId == SYNC_REPORTS_RESET) { - // For test purposes, force refresh of mSyncReports - mSyncReports.clear(); - accountId = SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY; - } else if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { - // -1 == reload the list if empty, otherwise exit immediately - if (mSyncReports.size() > 0) { - return; - } - } else { - // load a single account if it doesn't already have a sync record - if (mSyncReports.containsKey(accountId)) { - return; - } - } - - // setup to add a single account or all accounts - Uri uri; - if (accountId == SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY) { - uri = Account.CONTENT_URI; - } else { - uri = ContentUris.withAppendedId(Account.CONTENT_URI, accountId); - } - - final boolean oneMinuteRefresh - = Preferences.getPreferences(this).getForceOneMinuteRefresh(); - if (oneMinuteRefresh) { - Log.w(LOG_TAG, "One-minute refresh enabled."); - } - - // We use a full projection here because we'll restore each account object from it - Cursor c = resolver.query(uri, Account.CONTENT_PROJECTION, null, null, null); - try { - while (c.moveToNext()) { - Account account = Account.getContent(c, Account.class); - // The following sanity checks are primarily for the sake of ignoring non-user - // accounts that may have been left behind e.g. by failed unit tests. - // Properly-formed accounts will always pass these simple checks. - if (TextUtils.isEmpty(account.mEmailAddress) - || account.mHostAuthKeyRecv <= 0 - || account.mHostAuthKeySend <= 0) { - continue; - } - - // The account is OK, so proceed - AccountSyncReport report = new AccountSyncReport(); - int syncInterval = account.mSyncInterval; - - // If we're not using MessagingController (EAS at this point), don't schedule syncs - if (!mController.isMessagingController(account.mId)) { - syncInterval = Account.CHECK_INTERVAL_NEVER; - } else if (oneMinuteRefresh && syncInterval >= 0) { - syncInterval = 1; - } - - report.accountId = account.mId; - report.prevSyncTime = 0; - report.nextSyncTime = (syncInterval > 0) ? 0 : -1; // 0 == ASAP -1 == no sync - - report.syncInterval = syncInterval; - - // See if the account is enabled for sync in AccountManager - android.accounts.Account accountManagerAccount = - new android.accounts.Account(account.mEmailAddress, - AccountManagerTypes.TYPE_POP_IMAP); - report.syncEnabled = ContentResolver.getSyncAutomatically(accountManagerAccount, - EmailContent.AUTHORITY); - - // TODO lookup # new in inbox - mSyncReports.put(report.accountId, report); - } - } finally { - c.close(); - } - } - - /** - * Update list with a single account's sync times and unread count - * - * @param accountId the account being updated - * @param newCount the number of new messages, or -1 if not being reported (don't update) - * @return the report for the updated account, or null if it doesn't exist (e.g. deleted) - */ - private AccountSyncReport updateAccountReport(long accountId, int newCount) { - // restore the reports if lost - setupSyncReports(accountId); - synchronized (mSyncReports) { - AccountSyncReport report = mSyncReports.get(accountId); - if (report == null) { - // discard result - there is no longer an account with this id - Log.d(LOG_TAG, "No account to update for id=" + Long.toString(accountId)); - return null; - } - - // report found - update it (note - editing the report while in-place in the hashmap) - report.prevSyncTime = SystemClock.elapsedRealtime(); - report.setNextSyncTime(); - if (Email.DEBUG) { - Log.d(LOG_TAG, "update account " + report.toString()); - } - return report; - } - } - - /** - * when we receive an alarm, update the account sync reports list if necessary - * this will be the case when if we have restarted the process and lost the data - * in the global. - * - * @param restoreIntent the intent with the list - */ - private void restoreSyncReports(Intent restoreIntent) { - // restore the reports if lost - setupSyncReports(SYNC_REPORTS_ALL_ACCOUNTS_IF_EMPTY); - synchronized (mSyncReports) { - long[] accountInfo = restoreIntent.getLongArrayExtra(EXTRA_ACCOUNT_INFO); - if (accountInfo == null) { - Log.d(LOG_TAG, "no data in intent to restore"); - return; - } - int accountInfoIndex = 0; - int accountInfoLimit = accountInfo.length; - while (accountInfoIndex < accountInfoLimit) { - long accountId = accountInfo[accountInfoIndex++]; - long prevSync = accountInfo[accountInfoIndex++]; - AccountSyncReport report = mSyncReports.get(accountId); - if (report != null) { - if (report.prevSyncTime == 0) { - report.prevSyncTime = prevSync; - report.setNextSyncTime(); - } - } - } - } - } - - class ControllerResults extends Controller.Result { - @Override - public void updateMailboxCallback(MessagingException result, long accountId, - long mailboxId, int progress, int numNewMessages, - ArrayList addedMessages) { - // First, look for authentication failures and notify - //checkAuthenticationStatus(result, accountId); - if (result != null || progress == 100) { - // We only track the inbox here in the service - ignore other mailboxes - long inboxId = Mailbox.findMailboxOfType(MailService.this, - accountId, Mailbox.TYPE_INBOX); - if (mailboxId == inboxId) { - if (progress == 100) { - updateAccountReport(accountId, numNewMessages); - } else { - updateAccountReport(accountId, -1); - } - } - } - } - - @Override - public void serviceCheckMailCallback(MessagingException result, long accountId, - long mailboxId, int progress, long tag) { - if (result != null || progress == 100) { - if (result != null) { - // the checkmail ended in an error. force an update of the refresh - // time, so we don't just spin on this account - updateAccountReport(accountId, -1); - } - AlarmManager alarmManager = (AlarmManager)getSystemService(Context.ALARM_SERVICE); - reschedule(alarmManager); - int serviceId = mStartId; - if (tag != 0) { - serviceId = (int) tag; - } - stopSelf(serviceId); - } - } - } - - public class EmailSyncStatusObserver implements SyncStatusObserver { - @Override - public void onStatusChanged(int which) { - // We ignore the argument (we can only get called in one case - when settings change) - } - } - public static ArrayList getPopImapAccountList(Context context) { ArrayList providerAccounts = new ArrayList(); Cursor c = context.getContentResolver().query(Account.CONTENT_URI, Account.ID_PROJECTION, diff --git a/src/com/android/email/service/Pop3Service.java b/src/com/android/email/service/Pop3Service.java new file mode 100644 index 000000000..8d9bb63d7 --- /dev/null +++ b/src/com/android/email/service/Pop3Service.java @@ -0,0 +1,722 @@ +/* + * 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.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.email.provider.Utilities; +import com.android.emailcommon.AccountManagerTypes; +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.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.MessageColumns; +import com.android.emailcommon.provider.EmailContent.SyncColumns; +import com.android.emailcommon.provider.Mailbox; +import com.android.emailcommon.service.IEmailServiceCallback; +import com.android.emailcommon.utility.AttachmentUtilities; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; + +public class Pop3Service extends Service { + private static final String TAG = "Pop3Service"; + private static final int MAX_SMALL_MESSAGE_SIZE = (25 * 1024); + + @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 EmailServiceStub mBinder = new EmailServiceStub() { + + @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 setCallback(IEmailServiceCallback cb) throws RemoteException { + mCallbackList.register(cb); + } + }; + + @Override + public IBinder onBind(Intent intent) { + mBinder.init(this, sCallbackProxy); + 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); + synchronizeMailboxGeneric(context, account, folder); + // 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 + Utilities.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 + Utilities.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 + Utilities.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 void 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) { + return; + } + + // 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; + 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; + } + } + } + + // 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); + } + + /** + * 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); + } + + /** + * 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); + try { + // loop through messages marked as deleted + while (deletes.moveToNext()) { + EmailContent.Message oldMessage = + EmailContent.getContent(deletes, EmailContent.Message.class); + + // Finally, delete the update + Uri uri = ContentUris.withAppendedId(EmailContent.Message.DELETED_CONTENT_URI, + oldMessage.mId); + context.getContentResolver().delete(uri, null, null); + } + } finally { + deletes.close(); + } + } +} \ No newline at end of file diff --git a/src/com/android/email/service/PopImapSyncAdapterService.java b/src/com/android/email/service/PopImapSyncAdapterService.java index 121383f97..c28f0296a 100644 --- a/src/com/android/email/service/PopImapSyncAdapterService.java +++ b/src/com/android/email/service/PopImapSyncAdapterService.java @@ -35,6 +35,7 @@ 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 com.android.emailcommon.service.EmailServiceProxy; import java.util.ArrayList; @@ -88,7 +89,11 @@ public class PopImapSyncAdapterService extends Service { Account account = Account.restoreAccountWithId(context, mailbox.mAccountKey); if (account == null) return; try { - ImapService.synchronizeMailboxSynchronous(context, account, mailbox); + if (account.getProtocol(context).equals(HostAuth.SCHEME_IMAP)) { + ImapService.synchronizeMailboxSynchronous(context, account, mailbox); + } else { + Pop3Service.synchronizeMailboxSynchronous(context, account, mailbox); + } } catch (MessagingException e) { int cause = e.getExceptionType(); switch(cause) { @@ -117,52 +122,55 @@ public class PopImapSyncAdapterService extends Service { 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); - } + 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); } + 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); + if (mailboxId == Mailbox.NO_MAILBOX) { + // Update folders? + EmailServiceProxy service = + EmailServiceUtils.getServiceForAccount(context, null, acct.mId); + service.updateFolderList(acct.mId); + } + 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) { diff --git a/tests/src/com/android/email/ControllerProviderOpsTests.java b/tests/src/com/android/email/ControllerProviderOpsTests.java index a0beb27c1..372e53d26 100644 --- a/tests/src/com/android/email/ControllerProviderOpsTests.java +++ b/tests/src/com/android/email/ControllerProviderOpsTests.java @@ -64,12 +64,6 @@ public class ControllerProviderOpsTests extends ProviderTestCase2 ContentCache.invalidateAllCaches(); } - @Override - public void tearDown() throws Exception { - super.tearDown(); - mTestController.cleanupForTest(); - } - /** * Lightweight subclass of the Controller class allows injection of mock context */ @@ -488,44 +482,6 @@ public class ControllerProviderOpsTests extends ProviderTestCase2 return hostAuth; } - public void testIsMessagingController() { - Account account1 = ProviderTestUtils.setupAccount("account1", false, - mProviderContext); - account1.mHostAuthRecv = setupSimpleHostAuth("eas"); - account1.save(mProviderContext); - assertFalse(mTestController.isMessagingController(account1)); - Account account2 = ProviderTestUtils.setupAccount("account2", false, - mProviderContext); - account2.mHostAuthRecv = setupSimpleHostAuth("imap"); - account2.save(mProviderContext); - assertTrue(mTestController.isMessagingController(account2)); - Account account3 = ProviderTestUtils.setupAccount("account3", false, - mProviderContext); - account3.mHostAuthRecv = setupSimpleHostAuth("pop3"); - account3.save(mProviderContext); - assertTrue(mTestController.isMessagingController(account3)); - Account account4 = ProviderTestUtils.setupAccount("account4", false, - mProviderContext); - account4.mHostAuthRecv = setupSimpleHostAuth("smtp"); - account4.save(mProviderContext); - assertFalse(mTestController.isMessagingController(account4)); - // There should be values for all of these accounts in the legacy map - assertNotNull(mTestController.mLegacyControllerMap.get(account1.mId)); - assertNotNull(mTestController.mLegacyControllerMap.get(account2.mId)); - assertNotNull(mTestController.mLegacyControllerMap.get(account3.mId)); - assertNotNull(mTestController.mLegacyControllerMap.get(account4.mId)); - // The map should have the expected values - assertFalse(mTestController.mLegacyControllerMap.get(account1.mId)); - assertTrue(mTestController.mLegacyControllerMap.get(account2.mId)); - assertTrue(mTestController.mLegacyControllerMap.get(account3.mId)); - assertFalse(mTestController.mLegacyControllerMap.get(account4.mId)); - // This second pass should pull values from the cache - assertFalse(mTestController.isMessagingController(account1)); - assertTrue(mTestController.isMessagingController(account2)); - assertTrue(mTestController.isMessagingController(account3)); - assertFalse(mTestController.isMessagingController(account4)); - } - /** * TODO: releasing associated data (e.g. attachments, embedded images) */ diff --git a/tests/src/com/android/email/MessagingControllerUnitTests.java b/tests/src/com/android/email/MessagingControllerUnitTests.java deleted file mode 100644 index c4372b8c3..000000000 --- a/tests/src/com/android/email/MessagingControllerUnitTests.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (C) 2009 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; - -import com.android.emailcommon.mail.MockFolder; -import com.android.emailcommon.provider.Account; - -import android.content.ContentUris; -import android.net.Uri; -import android.test.AndroidTestCase; -import android.test.suitebuilder.annotation.SmallTest; - -/** - * This is a series of unit tests for the MessagingController class. - * - * Technically these are functional because they use the underlying provider framework. - */ -@SmallTest -public class MessagingControllerUnitTests extends AndroidTestCase { - - private long mAccountId; - private Account mAccount; - - /** - * Delete any dummy accounts we set up for this test - */ - @Override - protected void tearDown() throws Exception { - super.tearDown(); - - if (mAccount != null) { - Uri uri = ContentUris.withAppendedId( - Account.CONTENT_URI, mAccountId); - getContext().getContentResolver().delete(uri, null, null); - } - } - - /** - * MockFolder allows setting and retrieving role & name - */ - private static class MyMockFolder extends MockFolder { - private FolderRole mRole; - private String mName; - - public MyMockFolder(FolderRole role, String name) { - mRole = role; - mName = name; - } - - @Override - public String getName() { - return mName; - } - - @Override - public FolderRole getRole() { - return mRole; - } - } - - /** - * Create a dummy account with minimal fields - */ - private void createTestAccount() { - mAccount = new Account(); - mAccount.save(getContext()); - - mAccountId = mAccount.mId; - } - -} diff --git a/tests/src/com/android/email/RefreshManagerTest.java b/tests/src/com/android/email/RefreshManagerTest.java index 853c354b1..31384250b 100644 --- a/tests/src/com/android/email/RefreshManagerTest.java +++ b/tests/src/com/android/email/RefreshManagerTest.java @@ -62,12 +62,6 @@ public class RefreshManagerTest extends InstrumentationTestCase { mTarget.registerListener(mListener); } - @Override - protected void tearDown() throws Exception { - super.tearDown(); - mController.cleanupForTest(); - } - public void testRegisterUnregisterListener() { // mListener is already registered assertEquals(1, mTarget.getListenersForTest().size()); diff --git a/tests/src/com/android/email/activity/MailboxFinderTest.java b/tests/src/com/android/email/activity/MailboxFinderTest.java index 6087081db..1eff6e420 100644 --- a/tests/src/com/android/email/activity/MailboxFinderTest.java +++ b/tests/src/com/android/email/activity/MailboxFinderTest.java @@ -77,7 +77,6 @@ public class MailboxFinderTest extends InstrumentationTestCase { // MailboxFinder should unregister its listener when closed. checkControllerResultRemoved(mMockController); } - mMockController.cleanupForTest(); Controller.injectMockControllerForTest(null); } diff --git a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java b/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java index 4c182b9a0..21cc45436 100644 --- a/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java +++ b/tests/src/com/android/email/service/AttachmentDownloadServiceTests.java @@ -24,7 +24,6 @@ import com.android.email.EmailConnectivityManager; import com.android.email.provider.ProviderTestUtils; import com.android.email.service.AttachmentDownloadService.DownloadRequest; import com.android.email.service.AttachmentDownloadService.DownloadSet; -import com.android.email.service.EmailServiceUtils.NullEmailService; import com.android.emailcommon.provider.Account; import com.android.emailcommon.provider.EmailContent.Attachment; import com.android.emailcommon.provider.EmailContent.Message; @@ -71,7 +70,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { mService = new AttachmentDownloadService(); mService.mContext = mMockContext; mService.addServiceIntentForTest(mAccountId, new Intent(mContext, - NullEmailService.class)); + EmailServiceStub.class)); mAccountManagerStub = new AttachmentDownloadService.AccountManagerStub(null); mService.mAccountManagerStub = mAccountManagerStub; mService.mConnectivityManager = new MockConnectivityManager(mContext, "mock"); @@ -183,10 +182,12 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { mUsableSpace = usable; } + @Override public long getTotalSpace() { return mTotalSpace; } + @Override public long getUsableSpace() { return mUsableSpace; } @@ -195,6 +196,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { mMockFile.mLength = length; } + @Override public File[] listFiles() { return mFiles; } @@ -211,6 +213,7 @@ public class AttachmentDownloadServiceTests extends AccountTestCase { super("_mock"); } + @Override public long length() { return mLength; } diff --git a/tests/src/com/android/email/service/MailServiceTests.java b/tests/src/com/android/email/service/MailServiceTests.java deleted file mode 100644 index f716e314a..000000000 --- a/tests/src/com/android/email/service/MailServiceTests.java +++ /dev/null @@ -1,331 +0,0 @@ -/* - * Copyright (C) 2010 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.accounts.AccountManager; -import android.content.ComponentName; -import android.content.ContentResolver; -import android.content.ContentUris; -import android.content.ContentValues; -import android.content.Context; -import android.content.pm.PackageManager; - -import com.android.email.AccountTestCase; -import com.android.email.Controller; -import com.android.email.provider.AccountReconciler; -import com.android.email.provider.EmailProvider; -import com.android.email.provider.ProviderTestUtils; -import com.android.email.service.MailService.AccountSyncReport; -import com.android.emailcommon.provider.Account; -import com.android.emailcommon.provider.EmailContent; -import com.android.emailcommon.provider.HostAuth; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; - -/** - * Tests of the Email provider. - * - * You can run this entire test case with: - * runtest -c com.android.email.service.MailServiceTests email - */ -public class MailServiceTests extends AccountTestCase { - - EmailProvider mProvider; - Context mMockContext; - - public MailServiceTests() { - super(); - } - - @Override - public void setUp() throws Exception { - super.setUp(); - PackageManager pm = getContext().getPackageManager(); - pm.setComponentEnabledSetting( - new ComponentName(getContext(), EasTestAuthenticatorService.class), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP); - mMockContext = getMockContext(); - // Delete any test accounts we might have created earlier - deleteTemporaryAccountManagerAccounts(); - } - - @Override - public void tearDown() throws Exception { - super.tearDown(); - // Delete any test accounts we might have created earlier - deleteTemporaryAccountManagerAccounts(); - } - - /** - * Confirm that the test below is functional (and non-destructive) when there are - * prexisting (non-test) accounts in the account manager. - */ - public void testTestReconcileAccounts() { - Account firstAccount = null; - final String TEST_USER_ACCOUNT = "__user_account_test_1"; - Context context = getContext(); - try { - // Note: Unlike calls to setupProviderAndAccountManagerAccount(), we are creating - // *real* accounts here (not in the mock provider) - createAccountManagerAccount(TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX); - firstAccount = ProviderTestUtils.setupAccount(TEST_USER_ACCOUNT, true, context); - // Now run the test with the "user" accounts in place - testReconcileAccounts(); - } finally { - if (firstAccount != null) { - boolean firstAccountFound = false; - // delete the provider account - context.getContentResolver().delete(firstAccount.getUri(), null, null); - // delete the account manager account - android.accounts.Account[] accountManagerAccounts = AccountManager.get(context) - .getAccountsByType(TEST_ACCOUNT_TYPE); - for (android.accounts.Account accountManagerAccount: accountManagerAccounts) { - if ((TEST_USER_ACCOUNT + TEST_ACCOUNT_SUFFIX) - .equals(accountManagerAccount.name)) { - deleteAccountManagerAccount(accountManagerAccount); - firstAccountFound = true; - } - } - assertTrue(firstAccountFound); - } - } - } - - /** - * Note, there is some inherent risk in this test, as it creates *real* accounts in the - * system (it cannot use the mock context with the Account Manager). - */ - public void testReconcileAccounts() { - // Note that we can't use mMockContext for AccountManager interactions, as it isn't a fully - // functional Context. - Context context = getContext(); - - // Capture the baseline (account manager accounts) so we can measure the changes - // we're making, irrespective of the number of actual accounts, and not destroy them - android.accounts.Account[] baselineAccounts = - AccountManager.get(context).getAccountsByType(TEST_ACCOUNT_TYPE); - - // Set up three accounts, both in AccountManager and in EmailProvider - Account firstAccount = setupProviderAndAccountManagerAccount(getTestAccountName("1")); - setupProviderAndAccountManagerAccount(getTestAccountName("2")); - setupProviderAndAccountManagerAccount(getTestAccountName("3")); - - // Check that they're set up properly - assertEquals(3, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); - android.accounts.Account[] accountManagerAccounts = - getAccountManagerAccounts(baselineAccounts); - assertEquals(3, accountManagerAccounts.length); - - // Delete account "2" from AccountManager - android.accounts.Account removedAccount = - makeAccountManagerAccount(getTestAccountEmailAddress("2")); - deleteAccountManagerAccount(removedAccount); - - // Confirm it's deleted - accountManagerAccounts = getAccountManagerAccounts(baselineAccounts); - assertEquals(2, accountManagerAccounts.length); - - // Run the reconciler - ContentResolver resolver = mMockContext.getContentResolver(); - MailService.reconcileAccountsWithAccountManager(context, - makeExchangeServiceAccountList(), accountManagerAccounts, mMockContext); - - // There should now be only two EmailProvider accounts - assertEquals(2, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); - - // Ok, now we've got two of each; let's delete a provider account - resolver.delete(ContentUris.withAppendedId(Account.CONTENT_URI, firstAccount.mId), - null, null); - // ...and then there was one - assertEquals(1, EmailContent.count(mMockContext, Account.CONTENT_URI, null, null)); - - // Run the reconciler - MailService.reconcileAccountsWithAccountManager(context, - makeExchangeServiceAccountList(), accountManagerAccounts, mMockContext); - - // There should now be only one AccountManager account - accountManagerAccounts = getAccountManagerAccounts(baselineAccounts); - assertEquals(1, accountManagerAccounts.length); - // ... and it should be account "3" - assertEquals(getTestAccountEmailAddress("3"), accountManagerAccounts[0].name); - } - - public void testReconcileDetection() { - Context context = getContext(); - List providerAccounts; - android.accounts.Account[] accountManagerAccounts; - - android.accounts.Account[] baselineAccounts = - AccountManager.get(context).getAccountsByType(TEST_ACCOUNT_TYPE); - - // Empty lists match. - providerAccounts = new ArrayList(); - accountManagerAccounts = new android.accounts.Account[0]; - assertFalse(AccountReconciler.accountsNeedReconciling( - context, providerAccounts, accountManagerAccounts)); - - setupProviderAndAccountManagerAccount(getTestAccountName("1")); - accountManagerAccounts = getAccountManagerAccounts(baselineAccounts); - providerAccounts = makeExchangeServiceAccountList(); - - // A single account, but empty list on the other side is detected as needing reconciliation - assertTrue(AccountReconciler.accountsNeedReconciling( - context, new ArrayList(), accountManagerAccounts)); - assertTrue(AccountReconciler.accountsNeedReconciling( - context, providerAccounts, new android.accounts.Account[0])); - - // Note that no reconciliation should have happened though - we just wanted to detect it. - assertEquals(1, makeExchangeServiceAccountList().size()); - assertEquals(1, getAccountManagerAccounts(baselineAccounts).length); - - // Single account matches - no reconciliation should be detected. - assertFalse(AccountReconciler.accountsNeedReconciling( - context, providerAccounts, accountManagerAccounts)); - - // Provider: 1,2,3. AccountManager: 1, 3. - String username = getTestAccountName("2"); - ProviderTestUtils.setupAccount(getTestAccountName("2"), true, getMockContext()); - setupProviderAndAccountManagerAccount(getTestAccountName("3")); - - accountManagerAccounts = getAccountManagerAccounts(baselineAccounts); - providerAccounts = makeExchangeServiceAccountList(); - assertTrue(AccountReconciler.accountsNeedReconciling( - context, providerAccounts, accountManagerAccounts)); - } - - - /** - * Lightweight subclass of the Controller class allows injection of mock context - */ - public static class TestController extends Controller { - - protected TestController(Context providerContext, Context systemContext) { - super(systemContext); - setProviderContext(providerContext); - } - } - - /** - * Create a simple HostAuth with protocol - */ - private HostAuth setupSimpleHostAuth(String protocol) { - HostAuth hostAuth = new HostAuth(); - hostAuth.mProtocol = protocol; - return hostAuth; - } - - /** - * Initial testing on setupSyncReportsLocked, making sure that EAS accounts aren't scheduled - */ - public void testSetupSyncReportsLocked() { - // TODO Test other functionality within setupSyncReportsLocked - // Setup accounts of each type, all with manual sync at different intervals - Account easAccount = ProviderTestUtils.setupAccount("account1", false, mMockContext); - easAccount.mHostAuthRecv = setupSimpleHostAuth("eas"); - easAccount.mHostAuthSend = easAccount.mHostAuthRecv; - easAccount.mSyncInterval = 30; - easAccount.save(mMockContext); - Account imapAccount = ProviderTestUtils.setupAccount("account2", false, mMockContext); - imapAccount.mHostAuthRecv = setupSimpleHostAuth("imap"); - imapAccount.mHostAuthSend = setupSimpleHostAuth("smtp"); - imapAccount.mSyncInterval = 60; - imapAccount.save(mMockContext); - Account pop3Account = ProviderTestUtils.setupAccount("account3", false, mMockContext); - pop3Account.mHostAuthRecv = setupSimpleHostAuth("pop3"); - pop3Account.mHostAuthSend = setupSimpleHostAuth("smtp"); - pop3Account.mSyncInterval = 90; - pop3Account.save(mMockContext); - - // Setup the SyncReport's for these Accounts - MailService mailService = new MailService(); - mailService.mController = new TestController(mMockContext, getContext()); - try { - mailService.setupSyncReportsLocked(MailService.SYNC_REPORTS_RESET, mMockContext); - - // Get back the map created by MailService - HashMap syncReportMap = MailService.mSyncReports; - synchronized (syncReportMap) { - // Check the SyncReport's for correctness of sync interval - AccountSyncReport syncReport = syncReportMap.get(easAccount.mId); - assertNotNull(syncReport); - // EAS sync interval should have been changed to "never" - assertEquals(Account.CHECK_INTERVAL_NEVER, syncReport.syncInterval); - syncReport = syncReportMap.get(imapAccount.mId); - assertNotNull(syncReport); - assertEquals(60, syncReport.syncInterval); - syncReport = syncReportMap.get(pop3Account.mId); - assertNotNull(syncReport); - assertEquals(90, syncReport.syncInterval); - // Change the EAS account to push - ContentValues cv = new ContentValues(); - cv.put(Account.SYNC_INTERVAL, Account.CHECK_INTERVAL_PUSH); - easAccount.update(mMockContext, cv); - syncReportMap.clear(); - mailService.setupSyncReportsLocked(easAccount.mId, mMockContext); - syncReport = syncReportMap.get(easAccount.mId); - assertNotNull(syncReport); - // EAS sync interval should be "never" in this case as well - assertEquals(Account.CHECK_INTERVAL_NEVER, syncReport.syncInterval); - } - } finally { - mailService.mController.cleanupForTest(); - } - } - - /** - * Test that setupSyncReports will skip over poorly-formed accounts which can be left - * over after unit tests. - */ - public void testSetupSyncReportsWithBadAccounts() { - // Setup accounts that trigger each skip-over case - // 1: no email address - Account account1 = ProviderTestUtils.setupAccount("account1", false, mMockContext); - account1.mHostAuthRecv = setupSimpleHostAuth("imap"); - account1.mHostAuthSend = setupSimpleHostAuth("smtp"); - account1.mSyncInterval = 30; - account1.mEmailAddress = null; - account1.save(mMockContext); - // 2: no receiver hostauth - Account account2 = ProviderTestUtils.setupAccount("account2", false, mMockContext); - account2.mHostAuthRecv = null; - account2.mHostAuthSend = setupSimpleHostAuth("smtp"); - account2.mSyncInterval = 30; - account2.save(mMockContext); - // 3: no sender hostauth - Account account3 = ProviderTestUtils.setupAccount("account3", false, mMockContext); - account3.mHostAuthRecv = setupSimpleHostAuth("imap"); - account3.mHostAuthSend = null; - account3.mSyncInterval = 30; - account3.save(mMockContext); - - // Setup the SyncReport's for these Accounts - MailService mailService = new MailService(); - mailService.mController = new TestController(mMockContext, getContext()); - try { - mailService.setupSyncReportsLocked(MailService.SYNC_REPORTS_RESET, mMockContext); - // Get back the map created by MailService - it should be empty - HashMap syncReportMap = MailService.mSyncReports; - assertEquals(0, syncReportMap.size()); - } finally { - mailService.mController.cleanupForTest(); - } - - } -}