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