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