Start of IMAP conversion to Service architecture

* Handle startSync and loadMore
* Use SyncManager rather than MailService for periodic sync
  and upload sync
* First of many CL's to disentangle sync from UI
* Note that the large majority of this CL is a refactoring
  of IMAP specific code out of MessagingController and into
  ImapService; MessagingController will eventually be
  removed entirely from the app, as will much of Controller

Change-Id: I13546d0694479b33cf93c25920dedc1d38227f6c
This commit is contained in:
Marc Blank 2012-02-08 16:29:08 -08:00
parent 80535ef38c
commit c84467afe1
12 changed files with 2023 additions and 919 deletions

View File

@ -395,6 +395,17 @@
</intent-filter>
</service>
<service
android:name=".service.ImapService"
android:enabled="true"
android:permission="com.android.email.permission.ACCESS_PROVIDER"
>
<intent-filter>
<action
android:name="com.android.email.IMAP_INTENT" />
</intent-filter>
</service>
<!--Required stanza to register the EasAuthenticatorService with AccountManager -->
<service
android:name=".service.EasAuthenticatorService"

View File

@ -16,12 +16,6 @@
package com.android.emailcommon.service;
import com.android.emailcommon.Api;
import com.android.emailcommon.Device;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Policy;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
@ -29,6 +23,12 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import com.android.emailcommon.Api;
import com.android.emailcommon.Device;
import com.android.emailcommon.mail.MessagingException;
import com.android.emailcommon.provider.HostAuth;
import com.android.emailcommon.provider.Policy;
import java.io.IOException;
/**
@ -51,6 +51,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService {
// Private intent that will be used to connect to an independent Exchange service
public static final String EXCHANGE_INTENT = "com.android.email.EXCHANGE_INTENT";
public static final String IMAP_INTENT = "com.android.email.IMAP_INTENT";
public static final String AUTO_DISCOVER_BUNDLE_ERROR_CODE = "autodiscover_error_code";
public static final String AUTO_DISCOVER_BUNDLE_HOST_AUTH = "autodiscover_host_auth";
@ -64,6 +65,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService {
private final IEmailServiceCallback mCallback;
private Object mReturn = null;
private IEmailService mService;
private final boolean isRemote;
// Standard debugging
public static final int DEBUG_BIT = 1;
@ -82,6 +84,7 @@ public class EmailServiceProxy extends ServiceProxy implements IEmailService {
public EmailServiceProxy(Context _context, Class<?> _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;
}

View File

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

View File

@ -23,5 +23,6 @@
<sync-adapter xmlns:android="http://schemas.android.com/apk/res/android"
android:contentAuthority="com.android.email.provider"
android:accountType="com.android.email"
android:supportsUploading="false"
android:supportsUploading="true"
android:allowParallelSyncs="true"
/>

View File

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

View File

@ -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<Long, SortableMessage[]> sSearchResults =
new HashMap<Long, SortableMessage[]>();
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<SortableMessage>() {
@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<Message> messageList = new ArrayList<Message>();
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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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<Long> mailboxesToUpdate = new ArrayList<Long>();
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();
}
}
}
}